Typescript中的装饰器与应用(包含5.0新语法)

412 阅读7分钟

装饰器

装饰器是一种特殊类型的函数,它以@符号为前缀,后跟一个函数表达式。这个函数会在类声明之后立即执行

装饰器根据它们可以附着的代码元素类型分为类装饰器、方法装饰器、属性装饰器、参数装饰器和访问器装饰器。

要使用传统装饰器,需要在 tsconfig.json 中启用 experimentalDecorators 选项。

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

类装饰器

类装饰器函数有一个参数,即类的构造函数。

function logClass(target: Function) {
  console.log("Class logged: ", target.name); // MyClass
}

@logClass
class MyClass {
  // ...
}

类装饰器可以返回一个新类,这个新类将替换原始类。利用继承,我们可以做一些事情。

function logClass(target: new () => any) {
  return class extends target {
    name = "hello";
  };
}

@logClass
class MyClass {
  name: string = "";
}
const instance = new MyClass();
console.log(instance.name); // hello

方法装饰器

方法装饰器函数接收三个参数:类的原型对象、方法名和方法的属性描述符。

function methodLog(target: any, key: string, descriptor: PropertyDescriptor) {
  // target 是 MyClass 的原型对象
  // key 是 sayHello 方法名
  // descriptor 是 sayHello 方法的属性描述符
  console.log(target, key, descriptor);
}
class MyClass {
  @methodLog
  sayHello() {}
}

descriptorvalue 属性就是方法本身,所以我们可以利用它来修改方法。下面的示例中,用自定义的函数替换了原来的方法。所以instance.sayHello();执行的是自定义的函数,打印了 hello 和 world。

function methodLog(target: any, key: string, descriptor: PropertyDescriptor) {
  // 保存原来的方法
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log("hello"); // 打印 hello
    return originalMethod.apply(this, args); // 打印 world
  };
}
class MyClass {
  @methodLog
  sayHello() {
    console.log("world");
    return "log return";
  }
}
const instance = new MyClass();
instance.sayHello(); // log return

属性装饰器

属性装饰器函数接收两个参数:类的原型对象和属性名。

function logProperty(target: any, propertyName: string) {
  let value = target[propertyName];
  const getter = function () {
    console.log(`Get ${propertyName}: ${value}`); // 输出: Get message: Hello
    return value;
  };
  const setter = function (newVal: any) {
    console.log(`Set ${propertyName}: ${newVal}`); // 输出: Set message: World
    value = newVal;
  };

  Object.defineProperty(target, propertyName, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class MyClass {
  @logProperty
  message: string = "Hello";
}

const myClass = new MyClass();
console.log(myClass.message); // hello
myClass.message = "World";
console.log(myClass.message); // world

参数装饰器

参数装饰器函数接收三个参数:类的原型对象、属性名和参数在函数参数列表中的索引。

function Params(target: any, name: string, index: number) {
  // target: MyClass原型对象
  // name: sayHello
  // index: 0 该参数在函数参数列表中的索引为0
  console.log(target, name, index);
}
class MyClass {
  sayHello(@Params name: string) {}
}

元数据

上面我们简单介绍了装饰器,似乎没有感觉到装饰器有什么作用,当装饰器与元数据结合使用时,就可以做到很多事情了。

元数据是关于代码中的实体(如类、方法、属性等)的描述性信息。它可以在类定义的时候,给类添加一些属性,这些属性可以在运行时访问。nestjs 框架就是利用装饰器和元数据来实现依赖注入的。

使用元数据,需要先安装reflect-metadata包。

npm i reflect-metadata

引入这个包后,Reflect对象上会多出很多操作元数据的方法。

  1. Reflect.defineMetadata(metadataKey, metadataValue, target); 用于添加元数据,metadataKey:元数据的键,metadataValue:元数据值,target:目标对象。

  2. Reflect.getMetadata(metadataKey, target); 用于获取元数据,metadataKey:元数据的键,target:目标对象。

import "reflect-metadata";
class MyClass {}

Reflect.defineMetadata("key", "value", MyClass);

console.log(Reflect.hasMetadata("key", MyClass)); // 判断是否含有元数据,结果为:true
console.log(Reflect.getMetadata("key", MyClass)); // 获取元数据,结果为:value

emitDecoratorMetadata

在 ts 中,开启 emitDecoratorMetadata 后,类会添加一些内置的元数据。

通过下面的配置,开启 emitDecoratorMetadata 选项。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true // 开启 emitDecoratorMetadata 选项
  }
}

给类的成员添加装饰器(空的类装饰器就行)

import "reflect-metadata";
function Injectable(target: Function) {}
function propDecorator(target: object, propertyKey: string) {}

function paramDecorator(
  target: object,
  propertyKey: any,
  parameterIndex: number
) {}
function methodDecorator(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {}

@Injectable
class MyClass {
  @propDecorator
  name: string = "";
  constructor(
    @paramDecorator serviceA: string,
    @paramDecorator serviceB: string
  ) {}

  @methodDecorator
  myMethod(@paramDecorator a: number, @paramDecorator b: number): string {
    return "hello";
  }
}

之后,类中就会内置下面这些元数据。

  • design:type 用于属性的类型元数据
  • design:paramtypes 用于构建函数或方法参数的类型元数据
  • design:returntype 用于方法的返回类型元数据
import "reflect-metadata";
function Injectable(target: Function) {}
function propDecorator(target: object, propertyKey: string) {}

function paramDecorator(
  target: object,
  propertyKey: any,
  parameterIndex: number
) {}
function methodDecorator(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {}
@Injectable
class MyClass {
  @propDecorator
  name: string = "";
  constructor(
    @paramDecorator serviceA: string,
    @paramDecorator serviceB: string
  ) {}

  @methodDecorator
  myMethod(@paramDecorator a: number, @paramDecorator b: number): string {
    return "hello";
  }
}
// 获取属性 name 的类型构造函数 [Function: String]
console.log(Reflect.getMetadata("design:type", MyClass.prototype, "name"));

// 获取方法 myMethod 的类型构造函数 [Function: Function]
console.log(Reflect.getMetadata("design:type", MyClass.prototype, "myMethod"));

// 获取方法 MyClass constructor参数类型的构造函数 [ [Function: String], [Function: String] ]
console.log(Reflect.getMetadata("design:paramtypes", MyClass));

// 获取法法 myMethod 的参数类型的构造函数  [ [Function: Number], [Function: Number] ]
console.log(
  Reflect.getMetadata("design:paramtypes", MyClass.prototype, "myMethod")
);

// 获取 myMethod 的返回类型的构造函数 [Function: String]
console.log(
  Reflect.getMetadata("design:returntype", MyClass.prototype, "myMethod")
);

nest 中的装饰器

nest 中,装饰器可以让代码更通俗易懂,通过元数据注入,实现了控制反转。

  • @Controller 给一个类定义@Controller装饰器,目标类就变成了路由控制器。这个装饰器还可以传递参数,用于定义路由前缀。

下面代码中,@Controller("/user")就定义了路由前缀为/user的路由控制器。

@Controller("/user")
class User {}

@Controller 装饰器内部实现大概就是给目标类添加了路由前缀的元数据。之后 ioc 容器创建类实例的时候,可以从元数据中获取路由前缀,然后拼接成完整的路由。

function Controller(path: string) {
  return (target: any) => {
    Reflect.defineMetadata("prefix", path, target);
  };
}
  • @Get

方法装饰器也是同理,给方法添加@Get装饰器,目标方法就变成了路由方法。

@Controller("/user")
class User {
  @Get("login")
  login() {
    // do something
  }
}

内部会给目标方法添加路径的元数据。在创建 ioc 创建类实例的时候,就可以从元数据中获取路径,然后拼接成完整的路由。

function Get(path: string) {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata("path", path, target[propertyKey]);
    Reflect.defineMetadata("method", "GET", target[propertyKey]);
  };
}

添加了元数据后,在下面代码中遍历类的原型上的方法名,然后拿到目标方法,就可以从元数据中获取路径和路由方法名,通过express或者fastify进行路由绑定。

function init() {
  const prefix = Reflect.getMetadata("prefix", Controller) || "/";
  const controllerPrototype = User.prototype;

  // 遍历类的原型上的方法名
  for (const methodName of Object.getOwnPropertyNames(controllerPrototype)) {
    // 拿出login方法
    const method = controllerPrototype[methodName];

    // 取得此函数上绑定的方法名的元数据
    const httpMethod = Reflect.getMetadata("method", method); // GET
    // 取得此函数上绑定的路径的元数据
    const pathMetadata = Reflect.getMetadata("path", method); // login

    const routePath = path.posix.join("/", prefix, pathMetadata); // /user/login
    // TODO: 有了路由的方法名和路径,就可以进行路由的绑定了
  }
}

class-validator

这是一个可以用来验证类属性的库,通过装饰器来定义验证规则。

import { IsInt, Max, Min, validate } from "class-validator";

// 定义一个类
export class Post {
  @IsInt() // 必须是整数
  @Min(0) // 这个字段的值不能小于0
  @Max(10) // 这个字段的值不能大于10
  rating: number;
}

let post = new Post();
post.rating = 11; // 超过最大值

validate(post).then((errors) => {
  // 未通过验证
  if (errors.length > 0) {
    console.log("validation failed. errors: ", errors);
  } else {
    console.log("validation succeed");
  }
});

class-transformer

加入我们有一个 User dto 类

export class User {
  @IsInt()
  @Min(0)
  @Max(100)
  age: number;

  @IsString()
  name: string;
}

从请求体中获取到一个对象

const obj = {
  name: "张三",
  age: 18,
};

这时候如果想要将这个对象赋值给 User 类的实例,就需要使用 class-transformer

import { instanceToPlain, plainToClass } from "class-transformer";

export class User {
  @IsInt()
  @Min(0)
  @Max(100)
  age: number;

  @IsString()
  name: string;
}

const obj = {
  name: "张三",
  age: 18,
};

// 将普通对象转换为类实例
const ins = plainToClass(User, obj);
console.log(ins); // User类实例

这个 ins 也可以调用 class-validatorvalidate 方法,进行验证。

validate(ins).then((errors) => {
  // 未通过验证
  if (errors.length > 0) {
    console.log("validation failed. errors: ", errors);
  } else {
    console.log("validation passed.");
  }
});

如果想要将类实例转换为普通对象,可以使用 instanceToPlain 方法。

const newObj = instanceToPlain(ins);
console.log(newObj);

新版装饰器

TS5 支持的是 Stage3 装饰器,进入到 Stage3 的特性基本上就可以认为可以加入 js 标准了,在语法上,新版和传统装饰器有一些参数上的差异,要想使用新版装饰器,需要关闭experimentalDecorators选项。

{
  "compilerOptions": {
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false
  }
}

新版类装饰器

function logged(
  value: any,
  context: { kind: any; name: any; metadata: any; addInitializer: any }
) {
  console.log(value); // 目标类 [class myClass]

  // {kind:类型,name:类名,metadata:元数据,addInitializer:初始化函数}
  console.log(context);

  if (context.kind === "class") {
    return class extends value {};
  }
}

// @ts-ignore
@logged
class myClass {
  constructor(private a: number) {}
}
new myClass(1);
export {};

新版方法装饰器

function logged(
  value: Function, // 方法本身
  context: {
    kind: string; // method,表示类型是method
    name: string; // 函数名: sum
    static: boolean; // 是否是静态属性
    private: boolean; //是否是私有属性
    access: { has: Function; get: Function };
    metadata?: any; // 装饰器元数据
    addInitializer: Function;
  }
) {
  context.addInitializer(() => {
    // 初始化的时候执行
    console.log("initializing");
  });
  if (context.kind === "method") {
    //说明这是一个类的方法装饰器
    return function (...args: number[]) {
      console.log(`starting ${context.name} with arguments ${args.join(",")}`);
      const result = value.apply(null, args);
      console.log(`ending ${context.name}`);
      return result;
    };
  }
}

class Class {
  // @ts-ignore
  @logged
  sum(a: number, b: number) {
    return a + b;
  }
}
const result = new Class().sum(1, 2);
console.log(result);

新版属性装饰器

function logged(value: any, context: any) {
  console.log(value); // undefined
  console.log(context);
  // context如下
  /**
   * 
  kind: 'field',
  name: 'x',
  static: false,
  private: false,
  access: { has: [Function: has], get: [Function: get], set: [Function: set] },
  metadata: undefined,
  addInitializer: [Function (anonymous)]
   */
  if (context.kind === "field") {
    return function (initialValue: any) {
      console.log(`initializing ${context.name} with value ${initialValue}`);
      return initialValue + 1;
    };
  }
}
class Class {
  // @ts-ignore
  @logged x = 1;
}
console.log(new Class().x); // 2
export {};

新版访问器装饰器

function logged(value: any, context: any) {
  console.log("value", value); // [Function: set x]
  console.log("context", context);
  /**
   * 
   context {
    kind: 'setter',
    name: 'x',
    static: false,
    private: false,
    access: { has: [Function: has], set: [Function: set] },
    metadata: undefined,
    addInitializer: [Function (anonymous)]
  }
   */
  if (context.kind === "getter" || context.kind === "setter") {
    return function (...args: any[]) {
      console.log(`starting ${context.name} with arguments ${args.join(",")}`);
      const result = value.call(null, ...args);
      console.log(`ending ${context.name}`);
      return result;
    };
  }
}
class Class {
  @logged
  set x(args) {}
  @logged
  get x() {
    return 2;
  }
}
let clazz = new Class();
console.log(clazz.x);

export {};

总结

装饰器可以让我们的代码更加优雅易懂,期待未来能在前端生态中得到更多的应用。