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

前言

對每一個接口的傳入參數進行校驗,是一個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

Class-validaotr

有一個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-transformer作轉化

跟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

在Controller中使用

有了上面的工具,就能夠方便地在代碼中對傳入參數作校驗了,好比這樣: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會幫咱們生成這樣的文檔: 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後端項目,看代碼更容易理解。

相關文章
相關標籤/搜索