對每一個接口的傳入參數進行校驗,是一個Web後端項目的必備功能,有一個npm包叫Joi能夠很優雅的完成這個工做,好比這樣子:html
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 code return {name: 'bar'} }
如今問題就來了,若是傳入參數但願加多一個字段,咱們必須得修改2個地方,一個是Joi的校驗,一個是IFooParam類型的定義。有沒有好的辦法解決這個問題呢?git
有一個npm包叫class-validator, 是採用註解的方式進行校驗,底層使用的是老牌的校驗包validator.js。
此次試用,發現經過一些小包裝,竟然作到像Joi同樣優雅的寫法,並且更好用!github
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: string buttons: Button[] }
這裏定義了2個類,IRegister爲傳入參數,經過class-validator規定的註解方式作校驗,ORegister爲返回結果。web
class-validator官方提供的方式還不能直接對一個請求的body進行校驗,它要求必需要是IRegister類的一個對象,因此須要作一些處理。npm
跟class-validator的做者也開源了另一個包,叫class-transformer, 能夠將一個json轉成指定的類的對象,官方的例子是這樣的: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: ValidateUtil private 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作校驗,若是校驗錯誤將拋出錯誤,不然返回轉化後的對象。api
有了上面的工具,就能夠方便地在代碼中對傳入參數作校驗了,好比這樣:async
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文檔自動更新。
從同事的分享中得知一個廢棄的npm包,叫apidoc-plugin-ts, 能夠實現根據ts的interface定義來生成apidoc的。官方的例子:
filename: ./employers.ts export 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-ts,目前已經發布在npm上了,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後端項目,看代碼更容易理解。