使用 class-validator 替换 Joi 包的方法 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kaolalicai
V2EX    Node.js

使用 class-validator 替换 Joi 包的方法

  •  
  •   kaolalicai 2019-04-03 17:07:10 +08:00 4473 次点击
    这是一个创建于 2457 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    对每个接口的传入参数进行校验,是一个 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 code return {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: string buttons: 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: 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 做校验,如果校验错误将抛出错误,否则返回转化后的对象。

    在 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 会帮我们生成这样的文档: oldApidocDemo

    问题比较明显,当我们要新增一个参数时,需要修改一次类的定义,同时还要修改一次 apidoc 的注释,很烦,由于很烦,文档会慢慢变得没人维护,新同事就会吐槽没有文档或者文档太旧了。

    理想的情况是代码即文档,只需要修改类的定义,apidoc 文档自动更新。

    探索 apidoc 根据 class-validator 的定义生成

    从同事的分享中得知一个废弃的 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-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) 

    就会生成文档: demo

    后续新增字段,只需修改 IRegister 类的定义就,真正做到了修改一处,处处生效,代码即文档的效果。

    本文的 demo 代码在这里,这是一个简单的 web 后端项目,看代码更容易理解。

    2 条回复    2019-04-04 09:56:04 +08:00
    lizheming
        1
    lizheming  
       2019-04-03 19:09:49 +08:00
    看来大家都有这个痛点,之前我们用的是 https://github.com/SijieCai/ts-class-validator
    joesonw
        2
    joesonw  
       2019-04-04 09:56:04 +08:00
    都用过. joi 也能很好的推导出 typing 的. 但是稍微复杂一点点的还是得上 joi. 例如 Joi.when. 验证需求不复杂的话, class-validator 还是看的更顺眼
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2954 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 12:23 PVG 20:23 LAX 04:23 JFK 07:23
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86