前言
對每個接口的傳入參數進行校驗,是一個Web后端項目的必備功能,有一個npm包叫Joi可以很優雅的完成這個工作,比如這樣子:
const schema = {userId: Joi.string()
};
const {error, value} = Joi.validate({ userId: 'a string' }, schema);
復制代碼
我們使用Typescript是希望得到明確的類型定義,減少出錯的可能性。在一個后端項目中,給每個接口定義它的傳入參數結構以及返回結果的結構,是一件很值得做的事情,因為這樣給后續的維護帶來極大的便利。比如這樣子:
export type IFooParam = {userId: string
}export type IFooResponse = {name: string
}async foo (param: IFooParam): Promise<IFooResponse> {// Your business codereturn {name: 'bar'}
}
復制代碼
現在問題就來了,如果傳入參數希望加多一個字段,我們必須得修改2個地方,一個是Joi的校驗,一個是IFooParam類型的定義。有沒有好的辦法解決這個問題呢?
Class-validaotr
有一個npm包叫class-validator, 是采用注解的方式進行校驗,底層使用的是老牌的校驗包validator.js。
這次試用,發現通過一些小包裝,居然做到像Joi一樣優雅的寫法,而且更好用!
定義傳入/返回結構
import {Length, Min, Max} from 'class-validator'export class IRegister {@Length(11)phone: string@Length(2, 10)name: string@Min(18)@Max(50)age: number
}class Button {text: string
}export class ORegister {/*** user's id*/userId: stringbuttons: Button[]
}
復制代碼
這里定義了2個類,IRegister為傳入參數,通過class-validator規定的注解方式做校驗,ORegister為返回結果。
class-validator官方提供的方式還不能直接對一個請求的body進行校驗,它要求必須要是IRegister類的一個對象,所以需要做一些處理。
使用class-transformer做轉化
跟class-validator的作者也開源了另外一個包,叫class-transformer, 可以將一個json轉成指定的類的對象,官方的例子是這樣的:
import {plainToClass} from "class-transformer";let users = plainToClass(User, userJson); // to convert user plain object a single user. also supports arrays
復制代碼
利用這一點,我們寫一個小工具:
import * as classTransformer from 'class-transformer'
import {validate} from 'class-validator'
import * as lodash from 'lodash'export class ValidateUtil {private static instance: ValidateUtilprivate constructor () {}static getInstance () {return this.instance || (this.instance = new ValidateUtil())}async validate (Clazz, data): Promise<any> {const obj = classTransformer.plainToClass(Clazz, data)const errors = await validate(obj)if (errors.length > 0) {console.info(errors)throw new Error(lodash.values(errors[0].constraints)[0])}return obj}
}
復制代碼
這個小工具提供了一個validate方法,第一個參數是一個類定義,第二個是一個json,它先利用class-transformer將json轉成指定類的對象,然后使用class-validator做校驗,如果校驗錯誤將拋出錯誤,否則返回轉化后的對象。
在Controller中使用
有了上面的工具,就可以方便地在代碼中對傳入參數做校驗了,比如這樣:
static async register(ctx) {const iRegister = await ValidateUtil.getInstance().validate(IRegister, ctx.request.body)const oRegister = await UserService.register(iRegister)ctx.body = oRegister}
復制代碼
新問題
到了這里,完美地使用class-validator替換掉了Joi。
但是還有一個問題沒解決,也是之前一直遺留的問題。
我們使用apidoc編寫接口文檔,當新增或修改一個接口時,是通過編寫一段注釋,讓apidoc自動生成html文檔,將文檔地址發給前端,可以減少雙方的頻繁溝通,而且對前端的體驗也是非常好的。比如寫這樣一段注釋:
/*** @api {post} /user/registerOld registerOld* @apiGroup user* @apiName registerOld* @apiParam {String} name user's name* @apiParam {Number} age user's age* @apiSuccess {String} userId user's id */router.post('/user/registerOld', UserController.register)
復制代碼
問題比較明顯,當我們要新增一個參數時,需要修改一次類的定義,同時還要修改一次apidoc的注釋,很煩,由于很煩,文檔會慢慢變得沒人維護,新同事就會吐槽沒有文檔或者文檔太舊了。
理想的情況是代碼即文檔,只需要修改類的定義,apidoc文檔自動更新。
探索apidoc根據class-validator的定義生成
從同事的分享中得知一個廢棄的npm包,叫apidoc-plugin-ts, 可以實現根據ts的interface定義來生成apidoc的。官方的例子:
filename: ./employers.tsexport interface Employer {/*** Employer job title*/jobTitle: string;/*** Employer personal details*/personalDetails: {name: string;age: number;}
}@apiInterface (./employers.ts) {Person}
復制代碼
會轉化成:
@apiSuccess {String} jobTitle Job title@apiSuccess {Object} personalDetails Empoyer personal details@apiSuccess {String} personalDetails.name@apiSuccess {Number} personalDetails.age
復制代碼
雖然不知道為什么作者要廢棄它,但是它的思想很好,源碼也很有幫助。
給我的啟發是,參考這個npm包,寫一個針對class定義來生成apidoc的插件就行了。
造輪子: apidoc-plugin-class-validator
輪子的制造細節不適合在這里陳述,基本上參考apidoc-plugin-ts,目前已經發布在npm上了,apidoc-plugin-class-validator
使用apidoc-plugin-class-validator
以上面的注冊接口為例,使用方法:
/*** @api {post} /user/register register* @apiGroup user* @apiName register* @apiParamClass (src/user/io/Register.ts) {IRegister}* @apiSuccessClass (src/user/io/Register.ts) {ORegister}*/router.post('/user/register', UserController.register)
復制代碼
后續新增字段,只需修改IRegister類的定義就行,真正做到了修改一處,處處生效,代碼即文檔的效果。
本文的demo代碼在這里,這是一個簡單的web后端項目,看代碼更容易理解。