isaaxite / blog

I am a slow walker, but I never walk backwards.
35 stars 4 forks source link

装饰器实现参数的校验 #279

Open isaaxite opened 4 years ago

isaaxite commented 4 years ago

前言

需要用到的工具:

isaaxite commented 4 years ago

参数装饰器保存相关元数据

参数装饰器返回返回三个参数:1.构造函数的原型对象;2.参数所属方法名;3.参数的索引。

// 1.target: 构造函数的原型对象;
// 2.key: 参数所属方法名;
// 3.index: 参数的索引
const createRouteParamDecorator = (paramtype) => {
  return (data?: any, ...pipes: PipeTransform[]) => {
    return (target, key, index) => {
      const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, target, key) || {};
      Reflect.defineMetadata(
        ROUTE_ARGS_METADATA,
        assignMetadata(args, paramtype, index, data, ...pipes),
        target,
        key,
      );
    };
  };
};

export const Query = (property?: string, ...pipes: PipeTransform[]) => {
  return createRouteParamDecorator(RouteParamtypesEnum.QUERY)(property, ...pipes);
};
isaaxite commented 4 years ago

保存参数类型元数据

参数装饰器保存相关元数据分别保存了三个数据,但是为什么呢?自然是用于取出保存的某数据,而这个某数据就是参数定义的类型,比如现在有一个类Foo,并定义了一个setInfo成员方法,这个方法的参数定义了一个类型NameInfo

class NameInfo {
  ch: string;
  en: string;
}

class AgeInfo {
  num: number;
  born: string;
}

class PersonInfo {
  name: NameInfo;
  age: AgeInfo;
}

@Control('foo')
class Foo {
  private name: NameInfo;
  private age: AgeInfo;

  @Put('name')
  setInfo(
    @Query() _name: NameInfo,
    @Query() _age: AgeInfo,
  ): PersonInfo {
    this.name = _name;
    this.age = _age;
    return { name: this.name, age: this.age };
  }
}

setInfo编译后的代码如下:

tslib_1.__decorate([
    index_1.Put('name'),
    tslib_1.__param(0, param_dec_1.Query()),
    tslib_1.__param(1, param_dec_1.Query()),
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [NameInfo, AgeInfo]),
    tslib_1.__metadata("design:returntype", PersonInfo)
], Foo.prototype, "setInfo", null);

在定义的参数装饰器外,还另外多定义了三个tslib_1.__metadata装饰器。这个三个tslib_1.__metadata的定义分别是:

而这个tslib_1.__metadata方法相当于调用reflect-metadata库的Reflect.defineMetadata方法:

Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");

关于tslib_1.__metadata的具体实现可以参考附录中的tslib_1.__metadata

由上可知,在执行参数装饰器时,将方法的参数类型作为一个数组保存到构造函数的原型对象->参数所属方法名->metadataKey之中。

isaaxite commented 4 years ago

参数校验

根据构造函数原型对象引用、参数所属方法名、metadata key即刚刚的design:paramtypes就可以拿到刚刚存储的参数类型数组。

const handlerArgsTypes: any[] = Reflect.getMetadata(
  ReflectDefaultMetadata.DESGIN_PARAMTYPES,
  prototype,
  methodName,
);
// [NameInfo, AgeInfo]

然后在根据存储的参数索引即可拿到参数对应的类型。

有了类型信息,再加上请求是传过来的对象,就可以使用class-transformer的plainToClass方法将请求的参数转化成参数类型的实例:

clsObj = plainToClass(type, param)

有了实例,就可以使用class-validatorvalidate方法对请求参数进行校验。class-validator这个库提供了许多装饰器可以对类实例的成员进行校验,比如@isInt可以校验可以校验整型数据,@isString可以校验字符类型的数据

import { IsInt, IsString, validate} from 'class-validator';

class AgeInfo {
  @IsInt({ message: '$property必须是整型' })
  num: number;

  @IsString()
  born: string;
}

const errors = await validate(clsObj);
isaaxite commented 4 years ago

总结

  1. ts会默认将方法的参数类型作为一个数组以指定路径存储起来,这路径是以构造函数的原型对象引用、方法名、默认的metadata key(design:paramtypes)组成;

  2. 使用参数装饰器将参数的索引、所属方法名、构造函数的原型对象引用存储起来,后续就可以将参数类型取出;

  3. 使用class-transformer 库的plainToClass即可以将请求参数转化成参数对应类型的实例;

  4. 使用class-validator的校验装饰器对参数类型的成员进行类型描述,最后就可以根据class-transformer转化来的实例,配合class-validatorvadilate方法对请求参数进行校验,并可以定制错误信息。

isaaxite commented 4 years ago

附录

tslib_1.__metadata

// tslib_1.__metadata("design:paramtypes", [NameInfo, AgeInfo])
__metadata = function (metadataKey, metadataValue) {
  if (
    typeof Reflect === "object"
    && typeof Reflect.metadata === "function"
  ) {
    return Reflect.metadata(metadataKey, metadataValue);
  }
}

// Reflect.metadata的实现
function metadata(metadataKey, metadataValue) {
  function decorator(target, propertyKey) {
    if (!IsObject(target))
      throw new TypeError();
    if (!IsUndefined(propertyKey) && !IsPropertyKey(propertyKey))
      throw new TypeError();
    OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
  }
  return decorator;
}

// Reflect.defineMetadata的实现
function defineMetadata(metadataKey, metadataValue, target, propertyKey) {
  if (!IsObject(target))
    throw new TypeError();
  if (!IsUndefined(propertyKey))
    propertyKey = ToPropertyKey(propertyKey);
  return OrdinaryDefineOwnMetadata(metadataKey, metadataValue, target, propertyKey);
}

__decorate

__decorate = function (decorators, target, key, desc) {
  var c = arguments.length, 
      r = c < 3 
        ? target 
        : desc === null 
          ? desc = Object.getOwnPropertyDescriptor(target, key)
          : desc,
      d;
  if (
    typeof Reflect === "object" 
    && typeof Reflect.decorate === "function"
  ) {
    r = Reflect.decorate(decorators, target, key, desc);
  } else {
    for (var i = decorators.length - 1; i >= 0; i--) {
      if (d = decorators[i]) {
        r = (
          c < 3
            ? d(r)
            : c > 3 
              ? d(target, key, r) 
              : d(target, key)
        ) || r;
      }
    }
  }
  return c > 3 && r && Object.defineProperty(target, key, r), r;
};