0
点赞
收藏
分享

微信扫一扫

Egg.js 入参校验装饰器实践

落花时节又逢君to 2022-03-30 阅读 88
node.js

一、背景

我们在egg项目中,所有接口通常都存在请求入参校验的需求,例如:

  • 校验必填项字段,是否存在
  • 校验字段类型,是否与期望类型相符
  • 字符串数字,如果期望类型为数字,则需要转成数字类型,如果期望类型为字符串,则无需处理

……

程序员要追求dry(don't repeat yourself)不要重复自己,要提高代码重用率,缩减代码量。同时,提高代码可读性和可维护性,当需要修改时,只需修改一个地方就好。因此,我们对入参校验进行提取,并进行方案改造。

二、初步方案:封装公用参数校验方法

 

封装公用方法后,调用时还是比较啰嗦,对代码的侵入性还是比较强。

而我们希望的方案是:定义好接口入参数据结构、类型、相关字段是否必填后,传入参数对象,直接返回入参校验错误信息。

三、理想方案探索:装饰器

ES7 的 decorator 概念是从 Python 借来的,作用于一个目标函数,对这个目标函数做一些额外的操作,然后返回一个新的函数。ES7 中的 decorator 同样借鉴了这个语法糖,不过依赖于 ES5 的Object.defineProperty 方法。

1、Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象现有属性,并返回此对象。

语法:

Object.defineProperty(obj, prop, descriptor)

参数:

  • obj:要定义属性的对象
  • prop:要定义或修改的属性的名称或 Symbol
  • descriptor:要定义或修改的属性描述符,包含4个属性:

 eg:

var obj = {}; 
var descriptor = Object.create(null);  
Object.defineProperty(obj, "key", { //显式
 enumerable: false,
 configurable: false,
 writable: false,
 value: "static"
});

2、装饰器的分类

3、各类装饰器——应用场景

1)类装饰器

a、React 生命周期函数

* 页面初始化装饰,在使用 React 经常会在 componentDidMount 进行请求 api 数据

eg:

const InitDate = func => target => {
 const componentDidMount = target.prototype.componentDidMount;

 target.prototype.componentDidMount = () => {
     if (componentDidMount) componentDidMount();
         func();
     };
     return target;
};

@InitDate(action.init)
export default class Index extends Component { //... }
//等同于:
export default class Index extends Component {.
    componentDidMount() {
       action.init();
    }
}

* 同样的这种方式可以推广的更多的生命周期函数。例如:在 componentDidCatch 里进行错误捕捉等。

b、React 性能优化

编写一个装饰器,实现在 React shouldComponentUpdate 里进行性能优化,指定优化的字段:

//渲染优化
const RenderOptimization = (key: string | string[]) => (target): void => {
const scu = target.prototype.shouldComponentUpdate || (_ => true);

 target.prototype.shouldComponentUpdate = function (nextProps, nextState) {
      let flag = true;

      if (Array.isArray(key)) {
        for (let i = 0; i < key.length; i++) {
           flag = flag && (this.props[key[i]] === nextProps[key[i]]);
        }
      } else {
         flag = this.props[key] !== nextProps[key];
      }
      return flag && scu(nextProps, nextState);
 };
};

//在 props id 改变时才渲染
@RenderOptimization('id')
export default class Index extends Component { 
   //... 

2)方法装饰器

a、日志输出

eg:
const Log = name => (target, name, descriptor) => {
 const oldValue = descriptor.value;

 descriptor.value = function (...arg) {
      const val = oldValue.apply(this, arg);
      console.log(`调用函数:${name},参数为:${arg},结果为:${val}`);
      return val;
 };
 return descriptor;
};

export default class Index extends Component {
 @Log('add')
 add(state, payload) {
     return state.count + payload;
 }
}

b、异步函数装饰

对异步函数进行装饰实现,一些 loading 显示效果。例如:在 React 里会有很大的 onSubmit 提交,可以在提交之后显示 loading,提交成功后移除 loading。

eg:

const LoadingWrap = prefix => (target, name, descriptor) {
   const oldValue = descriptor.value;

   descriptor.value = function (...arg) {
       Toast.info(`${prefix}中`);
       const val = oldValue.apply(this, arg);
       Toast.success(`${prefix}${val ? '成功' : '失败'}`);
       return val;
   };
   return descriptor;
};
export default class Index extends Component {
   @LoadingWrap('提交')
   onSubmit = e => {
       return true;        //根据返回 true/false 来断定
   }
}

3)方法参数装饰器

a、入参校验

/app/controller/menuDev.ts:
...
@Post('/updateMenuDev')
    async updateMenuDev (@Body('menuDevUpdate') menuDevUpdate:MenuDevUpdate) {
       const { menuId } = menuDevUpdate 
       return await this.ctx.service.menuDev.updateMenuDev(menuDevUpdate, { menuId })
    }
...
 
/app/classes/menuDevUpdate.ts:
import { Number } from '../lib/decorator'
import MenuDev from './menuDev'

export default class MenuDevUpdate extends MenuDev {
    @Number
    menuId: number;

    modifyUser: string;

    modifyUserType: number;

    updatedAt: Date;
}
 
/app/classes/menuDev.ts:
import { IsInt, IsNotEmpty } from 'class-validator'

export default class MenuDev {
    @IsInt()
    menuPid: number;
 
    @IsInt()
    applicationId: number;
 
    menuPath: string;
 
    @IsNotEmpty()
    menuName: string;
 
    menuIcon: string;
 
    menuCategory: number;
 
    menuOrder: number;
 
    isHidden: boolean;
 
    menuDesc: string;
}

* 请求入参校验为:方法参数装饰器,以上代码是我们@Body的实现代码。

4)属性装饰器

a、数据格式转换

利用属性装饰,实现时间的格式化。

eg:

const TimeFormat = (format: string, p: string) => (target, key) => {
   let val = target[key];

   function getter() {
     if (typeof val === 'object') {
        val[p] = dayjs(val[p]).format(format);
     }
     return val;
   }
   function setter(newVal) {
      val = newVal;
   }
   Object.defineProperty(target, key, {
     get: getter,
     set: setter
   });
};
 
export default class Index extends Component {
 @TimeFormat('YYYY-MM-DD', 'time')
 state = {
     time: '2019-05-27 00:36:52' // => 2019-05-27
 }
}

四、元数据

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。主要的API如下:

 

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明时添加和读取元数据。在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现。

1)Reflect Metadata提案的目标

  • 许多用例(组合/依赖注入,运行时类型断言,反射/镜像,测试)都希望能够以一致的方式,向类中添加其他元数据
  • 为了使各种工具和库能够推理出元数据,需要一种一致的方法
  • 产生元数据的修饰符(nee。“Annotations”),通常需要与可变修饰符组合在一起
  • 元数据不仅应在对象上可用,而且可以通过具有相关陷阱的Proxy可用?
  • 定义生成元数据的装饰器,操作简单
  • 元数据应与ECMAScript的其他语言和运行时功能一致

2)句法

  • 元数据的声明式定义
  • class C {
     @Reflect.metadata(metadataKey, metadataValue)
     method() {
     }
    }
  • 元数据的命令式定义
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
  • 元数据的命令内省
let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method");

3)API

五、参数装饰器,入参转化与校验

  • class-transformer 用于数据格式的转换
  • class-validator 用于入参的数据验证

六、参数校验装饰器@Body代码实现

1、安装 reflect-metadata 包

npm install reflect-metadata –save

 2、在tsconfig.json配置文件中,添加如下两个配置项:

3、在controller层中:

1) 使用@Body装饰器,传入入参名称、实参、数据结构类型

/app/controller/menuDev.ts:
...
@Post('/updateMenuDev')
async updateMenuDev (@Body('menuDevUpdate') menuDevUpdate:MenuDevUpdate) {
    const { menuId } = menuDevUpdate 
    return await this.ctx.service.menuDev.updateMenuDev(menuDevUpdate, { menuId })
}
...

2) 定义好接口入参数据结构MenuDevUpdate类,与其字段类型,并在该类中为必填项字段、字符串数字期望转数字类型等,添加相应注解

/app/classes/menuDevUpdate.ts:

import { Number } from '../lib/decorator';
import MenuDev from './menuDev';
 
export default class MenuDevUpdate extends MenuDev {
   @Number
   menuId: number;
 
   modifyUser: string;
 
   modifyUserType: number;
 
   updatedAt: Date;
}
/app/classes/menuDev.ts:

import { IsInt, IsNotEmpty } from 'class-validator';

export default class MenuDev {
     @IsInt()
     menuPid: number;
 
     @IsInt()
     applicationId: number;
 
     menuPath: string;
 
     @IsNotEmpty()
     menuName: string;
 
     menuIcon: string;
 
     menuCategory: number;
 
     menuOrder: number;
 
     isHidden: boolean;
  
     menuDesc: string;
}

3) 使用class-transformer,将实参进行数据格式转换,转化成MenuDevUpdate类的数据类型

4) 使用class-validator,对实参进行数据验证:

  • 校验必填项字段,是否存在
  • 校验字段类型,是否与期望类型相符
  • 字符串数字,如果期望类型为数字,则需要转成数字类型,如果期望类型为字符串,则无需处理

……

4、定义@Body装饰器,在装饰器内部,使用Reflect Metadata,存储挂载在controller原型请求方法名上的元数据:

1) 挂载key为Symbol('routeArguments'),value 为如下格式的路由参数元数据(该元数据是累加的):

{ 
 [`${paramtype}:${index}`]: { 
      paramtype,      //路由参数类型(REQUEST=0、RESPONSE=1、BODY=2、QUERY=3、PARAM=4、HEADERS=5、FILE_STREAM=6)
      paramIndex: index, //路由-参数类型-索引
      propName: data,   //路由-参数位置-索引
      pipes             //转换器
 }
}

2) 当参数类型是:QUERY 或者PARAM时,key为Symbol('__routeArgumentTypesIndex__'),value为路由参数类型索引index的元数据;

以下是挂载在Constroller 原型上的元数据格式:

{ 
 …
 "请求方法名": {
    …
    Symbol('routeArguments'): { //路由参数(累加)
        [`${paramtype}:${index}`]: { //路由参数类型(REQUEST=0、RESPONSE=1、BODY=2、QUERY=3、PARAM=4、HEADERS=5、FILE_STREAM=6) 
             paramtype,  //路由-参数类型-索引
             paramIndex: index,            //路由-参数位置-索引
             propName: data, 
             pipes
         }
     },
     Symbol('routeArgumentTypesIndex'): index, //QUERY与PARAM参数独有,路由参数类型索引
     Symbol('commonParamNum'): {         //需要转成数字的字符串行数字入参字段(累加)
          [name: string]: {  
              decoratortype: number, 
              name: string,              //需要转化的字段名称
              type: any //期望转换后的字段类型
           }
      }
  }
}

 然后按照如下流程进行操作:

 

七、总结

综上,我们实现了:

1、自定义入参校验装饰器

2、使用Reflect Metadata元数据,获取存储:获取入参的类型信息

3、使用class-transformer,将入参对象进行类型转化

4、使用class-validator,进行入参类验证器

以上是我们项目中装饰器的一个小实践应用,分享出现与大家一起交流学习。

目前,我们正在进行egg.js装饰器中间件开发封装,主要内容如下:

 

请大家敬请期待(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 扩展介绍 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Egg:是为企业级框架和应用而生的Node.js 渐进式开发框架。它奉行『约定优于配置』,按照一套统一的约定进行应用开发,可以帮助开发团队和开发人员降低开发和维护成本。并且具有高度可扩展的插件机制,和内置多进程管理,提供了基于 Egg定制上层框架的能力。

官网:https://eggjs.org

Tower:是基于Egg封装的,适用于京东内部的Node.js MVC框架,集成了京东内部常用的系统工具,并打通了JDos部署,通过Tower框架,可以帮助开发团队和开发人员降低开发和部署成本。

官网:http://tower.jd.com/

举报

相关推荐

0 条评论