装饰器概念

装饰器-DecoratorsTypeScript 中是一种可以在不修改类代码的基础上通过添加标注的方式来对类型进行扩展的一种方式

  • 减少代码量
  • 提高代码扩展性、可读性和维护性

TypeScript 中,装饰器只能在类中使用

装饰器的使用

使用装饰器需要配置文件中启用experimentalDecorators: true

// 定义方法装饰器
function log(target: Function, name: string, descriptor: PropertyDescriptor) {
  /**
   * target : 被装饰的方法所属的类
   * name : 当前被装饰方法的名称
   * descriptor : 描述符
   */
  // 将原始方法提取出来
  let fn = descriptor.value
  // 定义新的方法
  descriptor.value = function (a: number, b: number) {
    // 调用原来的方法
    const result = fn(a, b)
    // 增加新的方法
    console.log(`${a}+${b}=${result}`)
    // 将结果返回
    return result
  }
}

class M {
  @log
  static add(a: number, b: number) {
    return a + b
  }
}

let v1 = M.add(1, 2)
console.log(v1)

image-20201215114331917

装饰器

装饰器 是一个函数,它可以通过 @装饰器函数 这种特殊的语法附加在 方法访问符属性参数 上,对它们进行包装,然后返回一个包装后的目标对象(方法访问符属性参数 ),装饰器工作在类的构建阶段,而不是使用阶段

function 装饰器1() {}
...

    @装饰器1
class MyClass {

    @装饰器2
    a: number;

    @装饰器3
    static property1: number;

    @装饰器4
    get b() { 
        return 1; 
    }

    @装饰器5
    static get c() {
        return 2;
    }

    @装饰器6
    public method1(@装饰器5 x: number) {
        //
    }

    @装饰器7
    public static method2() {}
}

装饰器的参数

function d1(target: Function) {
    console.log(typeof target, target);
}
function d11(target: Function) {
    console.log(typeof target, target, '1111111');
}

function d2(target: any, name: string) {
    console.log(typeof target, name);
}
function d3(target: any, name: string, descriptor: PropertyDescriptor) {
    console.log(typeof target, name, descriptor);
}
function d4(target: any, name: string, descriptor: PropertyDescriptor) {
    console.log(typeof target, name, descriptor);
}
function d5(target: any, name: string, index: number) {
    // name : 当前参数所在的方法
    console.log(typeof target, name, index);
}

@d1
@d11
class MyClass {

    @d2
    static property1: number;

    @d2
    a: number;

    @d3
    get b() { 
        return 1; 
    }

    @d3
    static get c() {
        return 2;
    }

    @d4
    public method1(@d5 x: number, @d5 y: number) {}

    @d4
    public static method2() {}

}
装饰器目标第一个参数第二个参数第三个参数
类装饰器应用于类的构造函数类的构造函数作为其唯一的参数××
方法装饰器应用于类的方法上静态方法:类的构造函数
实例方法:类的原型对象
方法名称方法描述符对象
属性装饰器应用于类的属性上静态方法:类的构造函数
实例方法:类的原型对象
属性名称×
访问器装饰器应用于类的访问器(gettersetter)上静态方法:类的构造函数
实例方法:类的原型对象
属性名称方法描述符对象
参数装饰器应用在参数上静态方法:类的构造函数
实例方法:类的原型对象
方法名称参数在函数参数列表中的索引

装饰器的执行顺序

image-20201215141610672

装饰器工厂

function log(type: string) {
    return function (target: Function, name: string, descriptor: PropertyDescriptor) {

        let value = descriptor.value;
        descriptor.value = function(x: number, y: number) {
            let result = value(x, y);

            console.log({
                type,
                name,
                x,
                y,
                result
            });

            return result;
        }

    }
}

class M {
    @log('log')
    static add(x: number, y: number) {
        return x + y;
    }

    @log('storage')
    static sub(x: number, y: number) {
        return x - y;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

export default {}

类的装饰器

装饰器的合理写法

// 装饰器本身是一个函数
function testDecorator<T extends new (...args: any[]) => any>(constructor: T) {
  return class extends constructor {
    age = 23
  }
}

@testDecorator
class Test {
  age: Number
  constructor(age: Number) {
    console.log(1)
    this.age = age
    console.log(2)
  }
}

const test = new Test(21)
console.log(test.age)
/**
 * 1
 * 2
 * 23
*/

扩展方法

利用工厂模式,在使用装饰器时就不是当作装饰器来使用,而是当成函数来使用,函数内传入类,将返回的结果作为类的constructor

function testDecorator() {
  return function <T extends new (...args: any[]) => any>(constructor: T) {
    return class extends constructor {
      age = 23
      getName() {
        return '小康'
      }
    }
  }
}

const Test = testDecorator()(
  class Test {
    age: Number
    constructor(age: Number) {
      this.age = age
    }
  }
)

const test = new Test(21)
console.log(test.age, test.getName()) // 23 小康

属性装饰器

属性装饰器修改的是原型上的属性的值,不能直接修改实例上的值。

function nameDecorator(target: any, key: string): any {
  target[key] = 23
}

class Test {
  @nameDecorator
  age = 21
}

const test = new Test()
console.log(test.age) // 21
console.log((test as any).__proto__.age) //23

方法装饰器

const userInfo: any = undefined
function catchError(msg: string) {
  return function (target: any, key: string, descriptor: PropertyDescriptor) {
    const fn = descriptor.value
    descriptor.value = function () {
      try {
        fn()
      } catch (e) {
        console.log(msg)
      }
    }
  }
}
class User {
  @catchError('userInfo.name不存在')
  getName() {
    return userInfo.name
  }
  @catchError('userInfo.age不存在')
  getAge() {
    return userInfo.age
  }
}
const user = new User()
user.getName()

元数据

元数据会被附加到指定的 方法 等数据之上,但是又不会影响 方法 本身的代码

使用 reflect-metadata

https://www.npmjs.com/package/reflect-metadata

首先,需要安装 reflect-metadata

npm install reflect-metadata

定义元数据

我们可以 方法 等数据定义元数据

设置

Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey)

  • metadataKey:meta 数据的 key
  • metadataValue:meta 数据的 值
  • target:meta 数据附加的目标
  • propertyKey:对应的 property key

调用方式

  • 通过 Reflect.defineMetadata 方法调用来添加 元数据

  • 通过 @Reflect.metadata 装饰器来添加 元数据

import "reflect-metadata"

@Reflect.metadata("n", 1)
class A {
    @Reflect.metadata("n", 2)
    public static method1() {
    }
  
  	@Reflect.metadata("n", 4)
  	public method2() {
    }
}

// or
Reflect.defineMetadata('n', 1, A);
Reflect.defineMetadata('n', 2, A, 'method1');

let obj = new A();
Reflect.defineMetadata('n', 3, obj);
Reflect.defineMetadata('n', 4, obj, 'method2');

console.log(Reflect.getMetadata('n', A));
console.log(Reflect.getMetadata('n', A, ));

获取

Reflect.getMetadata(metadataKey, target, propertyKey)

参数的含义与 defineMetadata 对应

使用元数据的 log 装饰器

import "reflect-metadata"

function L(type = 'log') {
  	return function(target: any) {
      	Reflect.defineMetadata("type", type, target);
    }
}
// 装饰器函数
function log(callback: Function) {
  	return function(target: any, name: string, descriptor: PropertyDescriptor) {
     	 	let value = descriptor.value;
      
      	let type = Reflect.getMetadata("type", target);

        descriptor.value = function(a: number, b: number) {
            let result = value(a, b);
          	if (type === 'log') {
              	console.log('日志:', {
                  name,
                  a,
                  b,
                  result
                })
            }
          	if (type === 'storage') {
                localStorage.setItem('storageLog', JSON.stringify({
                  name,
                  a,
                  b,
                  result
                }));
            }
            return result;
        }
    }
}

// 原始类
@L('log')
class M {
    @log
    static add(a: number, b: number) {
        return a + b;
    }
    @log
    static sub(a: number, b: number) {
        return a - b;
    }
}

let v1 = M.add(1, 2);
console.log(v1);
let v2 = M.sub(1, 2);
console.log(v2);

使用 emitDecoratorMetadata

tsconfig.json 中有一个配置 emitDecoratorMetadata,开启该特性,typescript 会在编译之后自动给 方法访问符属性参数 添加如下几个元数据

  • design:type:被装饰目标的类型
    • 成员属性:属性的标注类型
    • 成员方法:Function 类型
  • design:paramtypes
    • 成员方法:方法形参列表的标注类型
    • 类:构造函数形参列表的标注类型
  • design:returntype
    • 成员方法:函数返回值的标注类型
import "reflect-metadata"

function n(target: any) {
}
function f(name: string) {
    return function(target: any, propertyKey: string, descriptor: any) {
      	console.log( 'design type', Reflect.getMetadata('design:type', target, propertyKey) );
        console.log( 'params type', Reflect.getMetadata('design:paramtypes', target, propertyKey) );
        console.log( 'return type', Reflect.getMetadata('design:returntype', target, propertyKey) );
    }
}
function m(target: any, propertyKey: string) {

}

@n
class B {
    @m
    name: string;

    constructor(a: string) {

    }

    @f('')
    method1(a: string, b: string) {
        return 'a'
    }
}

编译后

__decorate([
    m,
    __metadata("design:type", String)
], B.prototype, "name", void 0);
__decorate([
    f(''),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", void 0)
], B.prototype, "method1", null);
B = __decorate([
    n,
    __metadata("design:paramtypes", [String])
], B);

不同请求方法的装饰器生成

import { Router } from 'express'
export const router = Router()

enum Method {
  get = 'get',
  post = 'post'
}

// 将类中原型上的路径和请求方法添加路由
export function controller(target: any) {
  for (let key in target.prototype) {
    const path = Reflect.getMetadata('path', target.prototype, key)
    const method: Method = Reflect.getMetadata('method', target.prototype, key)
    const handler = target.prototype[key]
    if (path && method && handler) {
      router[method](path, handler)
    }
  }
}
// 在类的原型对象中添加请求路径和请求方法
function getRequestDecorator(type: Method) {
  return function (path: string) {
    return function (target: any, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', type, target, key)
    }
  }
}

export const get = getRequestDecorator(Method.get)
export const post = getRequestDecorator(Method.post)
export const put = getRequestDecorator(Method.put)
export const del = getRequestDecorator(Method.delete)

使用

@controller
class LoginController {
  @post('/login')
  login(req: BodyRequest, res: Response) {}

  @get('/logout')
  logout(req: BodyRequest, res: Response) {}

  @get('/')
  home(req: BodyRequest, res: Response) {}
}

中间件装饰器

import 'reflect-metadata';
import { RequestHandler } from 'express';
import { CrowllerController, LoginController } from '../controller';

export function use(middleware: RequestHandler) {
  return function(target: CrowllerController | LoginController, key: string) {
    const originMiddlewares = Reflect.getMetadata('middlewares', target, key) || [];
    originMiddlewares.push(middleware);
    Reflect.defineMetadata('middlewares', originMiddlewares, target, key);
  };
}
export function controller(target: any) {
  for (let key in target.prototype) {
    const path = Reflect.getMetadata('path', target.prototype, key);
    const method: Method = Reflect.getMetadata('method', target.prototype, key);
    const handler = target.prototype[key];
    const middleware = Reflect.getMetadata('middleware', target.prototype, key);
    if (path && method && handler) {
      if (middleware && middleware.length) {
        router[method](path, ...middleware, handler);
      } else {
        router[method](path, handler);
      }
    }
  }
}

使用

const checkLogin = (req: Request, res: Response, next: NextFunction) => {
  const isLogin = req.session ? req.session.login : false;
  if (isLogin) {
    next();
  } else {
    res.json(getResponseData(null, '请先登录'));
  }
};

@controller
class LoginController {
  @post('/login')
  @use(checkLogin)
  login(req: BodyRequest, res: Response) {}

  @get('/logout')
  @use(checkLogin)
  logout(req: BodyRequest, res: Response) {}

  @get('/')
  @use(checkLogin)
  home(req: BodyRequest, res: Response) {}
}