对每一个接口的传入参数进行校验,是一个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后端项目,看代码更容易理解。