Skip to content

fengmk2/egg-typebox-validate

 
 

Repository files navigation

egg-typebox-validate

NPM version build status Test coverage Known Vulnerabilities npm download

基于 typeboxajv 封装的 egg validate 插件。

为什么有这个项目

一直以来,在 typescript 的 egg 项目里,对参数校验 ctx.validate 是比较难受的,比如:

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    // 写一遍 js 的类型校验
    ctx.validate({
      id: 'string',
      name: {
        type: 'string',
        required: false,
      },
      timestamp: {
        type: 'number',
        required: false,
      },
    }, ctx.params);
    
    // 写一遍 ts 的类型定义,为了后面拿参数定义
    const params: {
      id: string;
      name?: string;
      timestamp: number;
    } = ctx.params;
    ...
    ctx.body = params.id;
  }
}

export default HomeController;

可以看到这里我们写了两遍的类型定义,一遍 js 的定义(用 parameter 库的规则),另一遍用 ts 的方式来强转我们的参数类型,方便我们后面写代码的时候能得到 ts 的类型效果。 对于简单的类型写起来还好,但是对于复杂点的参数定义,开发体验就不是那么好了。

这就是这个库想要解决的问题,对于参数校验,写一遍类型就够了:

+ import { Static, Type } from '@sinclair/typebox';

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    // 写 js 类型定义
-   ctx.validate({
-     id: 'string',
-     name: {
-       type: 'string',
-       required: false,
-     },
-     timestamp: {
-       type: 'number',
-       required: false,
-     },
-   }, ctx.params);

+   const paramsSchema = Type.Object({
+     id: Type.String(),
+     name: Type.String(),
+     timestamp: Type.Integer(),
+   });
    // 直接校验
+   ctx.tValidate(paramsSchema, ctx.params);
    // 不用写 js 类型定义
+   const params: Static<typeof paramsSchema> = ctx.params;
-   const params: {
-     id: string;
-     name?: string;
-     timestamp: number;
-   } = ctx.params;
    ...
    ctx.body = params.id;
  }
}

export default HomeController;

Static<typeof typebox> 推导出的 ts 类型:

tpian

怎么使用

  1. 安装
npm i egg-typebox-validate -D
  1. 在项目中配置
// config/plugin.ts
const plugin: EggPlugin = {
  typeboxValidate: {
    enable: true,
    package: 'egg-typebox-validate',
  },
};
  1. 在业务代码中使用
+ import { Static, Type } from '@sinclair/typebox';

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    
+   const paramsSchema = Type.Object({
+     id: Type.String(),
+     name: Type.String(),
+     timestamp: Type.Integer(),
+   });
    // 直接校验
+   ctx.tValidate(paramsSchema, ctx.params);
    // 不用写 js 类型定义
+   const params: Static<typeof paramsSchema> = ctx.params;

    ...
  }
}

export default HomeController;

除了类型定义 write once 外,还有更多好处

  1. 类型组合方式特别香,能解决很多 DRY(Don't Repeat Yourself) 问题。比如有几张 db 表,都定义了 name 必填和 description 选填,那这个规则可以在各个实体类方法中被组合了。

Show me the code!

export const TYPEBOX_NAME_DESC_OBJECT = Type.Object({
  name: Type.String(),
  description: Type.Optional(Type.String()),
});

// type NameAndDesc = { name: string; description?: string }
type NameAndDesc = Static<typeof TYPEBOX_NAME_DESC_OBJECT>;

// controller User
async create() {
  const { ctx } = this;
  const USER_TYPEBOX = Type.Intersect([
    TYPEBOX_NAME_DESC_OBJECT,
    Type.Object({ avatar: Type.String() }),
  ])
  ctx.tValidate(USER_TYPEBOX, ctx.request.body);
  
  // 在编辑器都能正确得到提示
  // type User = { name: string; description?: string } & { avatar: string }
  const { name, description, avatar } = ctx.request.body as Static<typeof USER_TYPEBOX>;
  ...
}

// controller Photo
async create() {
  const { ctx } = this;
  const PHOTO_TYPEBOX = Type.Intersect([
    TYPEBOX_NAME_DESC_OBJECT,
    Type.Object({ location: Type.String() }),
  ])
  ctx.tValidate(PHOTO_TYPEBOX, ctx.request.body);

  // 在编辑器都能正确得到提示
  // type Photo = { name: string; description?: string } & { location: string }
  const { name, description, location } = ctx.request.body as Static<typeof PHOTO_TYPEBOX>;
  ...
}
  1. 校验规则使用的是业界标准的 json-schema 规范,内置很多开箱即用的类型
'date-time', 
'time',
'date',
'email',
'hostname',
'ipv4',
'ipv6',
'uri',
'uri-reference',
'uuid',
'uri-template',
'json-pointer',
'relative-json-pointer',
'regex'
  1. 写定义的时候写的是 js 对象(Type.Number()),有类型提示,语法也比较简单,有提示不容易写错;写 parameter 规范的时候,写字符串('nunber')有时候会不小心写错 😂,再加上它对于复杂嵌套对象的写法还是比较困难的,我每次都会查文档,官方的文档也不全。但是 typebox,就很容易举一反三了。

与 egg-validate 性能比较

egg-typebox-validate 底层使用的是 ajv, 官网上宣称是 The fastest JSON validator for Node.js and browser.

但是 parameter 跑了 benchmark 后,它完败(不是一个数量级的),毕竟底层实现是完全不一样的,一个是按标准 json-schema 规范去做解析的,另一个是轻量简单处理。

对于最简单的 case:

suite
  .add('#ajv', function() {
    const rule = Type.Object({
      name: Type.String(),
      description: Type.Optional(Type.String()),
      location: Type.Enum({shanghai: 'shanghai', hangzhou: 'hangzhou'}),
    })
    ajv.validate(rule, DATA);
  })
  .add('#parameter', function() {
    const rule = {
      name: 'string',
      description:
        type: 'string',
        required: false,
      },
      location: ['shanghai', 'hangzhou'],
    }
    p.validate(rule, DATA);
  })

在 MacBook Pro(2.2 GHz 六核Intel Core i7)上,跑出来结果是:

#ajv x 728 ops/sec ±6.82% (73 runs sampled)
#parameter x 2,699,754 ops/sec ±2.30% (86 runs sampled)
Fastest is #parameter

翻译一下就是:

  • ajv 每跑一次大概是 1.3ms
  • parameter 每跑一次大概是 0.0003ms

从 egg-validate 迁移到这个库的成本

  1. 会有 1ms 左右的性能损耗。但不管怎么说,ajv 是 node 最快的 json-schema validator 了。
  2. 把原来字符串式 js 对象写法迁移到 typebox 的对象写法。typebox 的写法还算简单和容易举一反三。

总结

切换到 egg-typebox-validate 校验后:

  1. 可以解决 ts 项目中参数校验代码写两遍类型的问题,提升代码重用率,可维护性等问题。
  2. 用标准 json-schema 来做参数校验,内置更多类型
  3. 建议渐进式迁移,从部分简单方法开始把 ctx.validate 改成 ctx.tValidate

API

  1. ctx.tValidate 参数校验失败后,抛出错误,内部实现(错误码、错误标题等)逻辑和 ctx.validate 的保持一致
+ import { Static, Type } from '@sinclair/typebox';

ctx.tValidate(Type.Object({
  name: Type.String(),
}), ctx.request.body);
  1. ctx.tValidateWithoutThrow 直接校验,不抛出错误
+ import { Static, Type } from '@sinclair/typebox';

const valid = ctx.tValidateWithoutThrow(Type.Object({
  name: Type.String(),
}), ctx.request.body);

if (valid) {
  ...
} else {
  ...
}

怎么写 typebox 定义

参考 https://github.com/sinclairzx81/typebox#types

License

MIT

About

typebox validator for egg

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 56.2%
  • JavaScript 43.8%