TS 进阶 - 实际应用 03

2023-05-17 20:42:04 浏览数 (1)

# 装饰器与反射元数据

# 装饰器

装饰器的本质是一个函数,只不过它的入参时提前确定好的。TypeScript 中的装饰器目前只能在类及类成员上使用

代码语言:javascript复制
function Deco() {}

@Deco
class Foo {}

实际使用中更多的是装饰器工厂:

代码语言:javascript复制
function Deco() {
  return () => {}
}

@Deco()
class Foo {}
// 程序执行时会先执行 Deco(),再用内部返回的函数作为装饰器的实际逻辑
// 以此可以通过入参来灵活调整装饰器的作用

TypeScript 中的装饰器可以分为:类装饰器、方法装饰器、访问符装饰器、属性装饰器和参数装饰器。

  • 类装饰器
    • 直接作用在类上的装饰器
    • 执行时的入参只有一个,即被装饰的类
    • 可以通过类装饰器来覆盖类的属性和方法,如果在类装饰器中返回一个新的类,甚至可以篡改整个类的实现
代码语言:javascript复制
function AddMethod(): ClassDecorator {
  return (target: any) => {
    target.prototype.newInstanceMethod = () => {
      console.log('new instance method');
    };
    target.newStaticMethod = () => {
      console.log('new static method');
    };
  };
}

function AddProperty(value: string): ClassDecorator {
  return (target: any) => {
    target.prototype.newInstanceProperty = value;
    target.newStaticProperty = `static ${value}`;
  };
}

@AddProperty('hello')
@AddMethod()
class Foo {
  a = 1;
}

const foo = new Foo();
foo.newInstanceMethod(); // new instance method
foo.newInstanceProperty; // hello
Foo.newStaticMethod(); // new static method
Foo.newStaticProperty; // static hello

因为函数返回了一个 ClassDecorator,因此装饰器是一个 Decorator Factory,在实际执行时需要以 @Deco() 形式调用。

也可以在装饰中返回一个子类

代码语言:javascript复制
const OverrideBar = (target: any) => {
  return class extends target {
    print() {
      console.log('nothing');
    }
    overridePrint() {
      console.log('This is Override Bar');
    }
  };
};

@OverrideBar
class Bar {
  print() {
    console.log('This is Bar');
  }
}

new Bar().print(); // nothing
new Bar().overridePrint(); // This is Override Bar

  • 方法装饰器
    • 入参包括类的原型,方法名和方法的属性描述符
    • 通过属性描述符可以控制这个方法的内部实现、可变性等
代码语言:javascript复制
function ComputeProfiler(): MethodDecorator {
  return (
    _target,
    methodIdentifier,
    descriptor: TypedPropertyDescriptor<any>
  ) => {
    const originalMethodImpl = descriptor.value!;
    descriptor.value = async function (...args: unknown[]) {
      const start = new Date();
      const res = await originalMethodImpl.apply(this, args);
      const end = new Date();
      console.log(
        `Method ${methodIdentifier.toString()} took ${
          end.getTime() - start.getTime()
        }ms`
      );
      return res;
    };
  };
}

class Foo {
  @ComputeProfiler()
  async fetch() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('hello');
      }, 2000);
    });
  }
}

(async () => {
  const foo = new Foo();
  await foo.fetch();
})();
// after 2s "Method fetch took 2005ms" will be printed

  • 访问符装饰器
    • getter 在访问属性 value 时触发,settervalue 被赋值时触发
    • 访问器本质还是方法装饰器
    • 注意,访问符装饰器只能同时应用在一对 getter/setter 的其中一个,因为不论装饰哪一个,装饰器入参的属性描述符都会包括 gettersetter 方法
代码语言:javascript复制
function HijackSetter(val: string): MethodDecorator {
  return (target, methodIdentifier, descriptor: any) => {
    const originalSetter = descriptor.set;
    descriptor.set = function (newValue: string) {
      const composed = `Raw: ${newValue}, Actual:${val}-${newValue}`;
      originalSetter.call(this, composed);
      console.log(`HijackSetter: ${composed}`);
    };
  };
}

class Foo {
  _value!: string;

  get value() {
    return this._value;
  }

  @HijackSetter('hello')
  set value(newValue: string) {
    this._value = newValue;
  }
}

const foo = new Foo();
foo.value = 'world';
// HijackSetter: Raw: world, Actual:hello-world

  • 属性装饰器
    • 属性装饰器在独立使用时能力非常有限
    • 其入参只有类的原型和属性名称,返回值会被忽略
    • 但是,仍然可以通过直接在类的原型上赋值来修改属性
代码语言:javascript复制
function ModifyNickName(): PropertyDecorator {
  return (target: any, propertyIdentifier) => {
    target[propertyIdentifier] = 'Cell';
    target['otherName'] = 'Cellinlab';
  };
}

class Foo {
  @ModifyNickName()
  nickName!: string;

  constructor() {}
}

const foo = new Foo();
console.log(foo.nickName); // Cell
// @ts-expect-error
console.log(foo.otherName); // Cellinlab

  • 参数装饰器
    • 参数装饰器包括了构造函数的参数装饰器和方法的参数装饰器
    • 其入参包括类的原型、参数所在的方法名与参数在函数中的索引值(即第几个参数)
    • 在单独使用时,作用也比较有限
代码语言:javascript复制
function CheckParam(): ParameterDecorator {
  return (target, methodIdentifier, index) => {
    console.log(target, methodIdentifier, index);
  };
}

class Foo {
  handler(@CheckParam() input: string) {
    console.log(input);
  }
}

const foo = new Foo();
foo.handler('hello');
// Foo: {} 'handler' 0
// hello

# 装饰器的执行机制

装饰器本质是一个函数,只要在类上定义了它,即使不去实例化类或读取静态成员,也会正常执行。很多时候,不会实例化具有装饰器的类,而是通过反射元数据的能力来消费。

代码语言:javascript复制
@Cls()
class Foo {
  constructor(@Param() init?: string) {}

  @Prop()
  prop!: string;

  @Method()
  handler(@Param() input: string) {}
}

// 以上代码会被编译为
// "use strict";
// var __decorate = (this && this.__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;
// };
// var __metadata = (this && this.__metadata) || function (k, v) {
//     if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
// };
// var __param = (this && this.__param) || function (paramIndex, decorator) {
//     return function (target, key) { decorator(target, key, paramIndex); }
// };
// let Foo = class Foo {
//     constructor(init) { }
//     handler(input) { }
// };
// __decorate([
//     Prop(),
//     __metadata("design:type", String)
// ], Foo.prototype, "prop", void 0);
// __decorate([
//     Method(),
//     __param(0, Param()),
//     __metadata("design:type", Function),
//     __metadata("design:paramtypes", [String]),
//     __metadata("design:returntype", void 0)
// ], Foo.prototype, "handler", null);
// Foo = __decorate([
//     Cls(),
//     __param(0, Param()),
//     __metadata("design:paramtypes", [String])
// ], Foo);

在 TypeScript 官方文档中对应顺序给出了详细的定义:

  1. 参数装饰器,然后依次是方法装饰器、访问符装饰器或属性装饰器应用到每个实例成员
  2. 参数装饰器,然后依次是方法装饰器、访问符装饰器或属性装饰器应用到每个静态成员
  3. 参数装饰器应用到构造函数
  4. 类装饰器应用到类

# 反射 Reflect

Reflect 在 ES6 中首次引入,主要是为了配合 Proxy 保留一份方法原始的实现逻辑:

代码语言:javascript复制
Proxy(target, {
  set: function(target, property, value, receiver) {
    var success = Reflect.set(target, property, value, receiver);
    if (success) {
      console.log("property "   property   " on "   target   " set to "   value);
    }
    return success;
  }
});

Proxy 将修改这个对象的 set 方法,但我们可以通过 Reflect.set 方法获取原本的默认实现,先执行完默认实现逻辑再添加自己的额外逻辑。

Proxy 上的这些方法会一一对应到 Reflect 中。

通过反射,在运行时去修改了程序的行为,这就是反射的核心:在程序运行时去检查以及修改程序行为

如通过反射来实例化一个类:

代码语言:javascript复制
// 正常情况
const foo = new Foo();
foo.hello();

// 基于反射
const foo = Reflect.construct(Foo, []);
const hello = Reflect.get(foo, 'hello');
Reflect.apply(hello, foo, []);

# 反射元数据 Reflect Metadata

反射元数据为顶级对象 Reflect 新增了一批专用于元数据读写的 API,如 Reflect.defineMetadataReflect.getMetadata 等。可以将元数据理解为用于描述数据的数据,如某个方法的参数信息、返回值信息就可以称为该方法的元数据。

为类或类属性添加元数据后,构造函数会具有 [[Metadata]] 属性,该属性内部包含一个 Map 结构,键为属性键,值为元数据键值对。静态成员的元数据信息存储于构造函数,而实例成员的元数据信息存储与构造函数的原型上。

代码语言:javascript复制
import 'reflect-metadata';

class Foo {
  handler() {}
}

Reflect.defineMetadata('class:key', 'class metadata', Foo);
Reflect.defineMetadata('method:key', 'method metadata', Foo, 'handler');
Reflect.defineMetadata(
  'proto:method:key',
  'proto method metadata',
  Foo.prototype,
  'handler'
);

// defineMetadata 参数包括元数据 Key,元数据 Value,目标类 Target 以及一个可选的属性

console.log(Reflect.getMetadataKeys(Foo)); // [ 'class:key' ]
console.log(Reflect.getMetadataKeys(Foo, 'handler')); // [ 'method:key' ]
console.log(Reflect.getMetadataKeys(Foo.prototype, 'handler')); // [ 'proto:method:key' ]

console.log(Reflect.getMetadata('class:key', Foo)); // class metadata
console.log(Reflect.getMetadata('method:key', Foo, 'handler')); // method metadata
console.log(Reflect.getMetadata('proto:method:key', Foo.prototype, 'handler')); // proto method metadata

反射元数据是实现属性装饰器中提到的“委托”能力的基础。在属性装饰器中注册一个元数据,然后在真正实例化这个类时,可以拿到类原型上的元数据,以此对实例化完毕的类再进行额外的操作。

考虑这些,反射元数据中直接就内置了基于装饰器的调用方式:

代码语言:javascript复制
@Reflect.metadata('class:key', 'METADATA_IN_CLASS')
class Foo {
  @Reflect.metadata('prop:key', 'METADATA_IN_PROP')
  prop: string = 'prop';

  @Reflect.metadata('method:key', 'METADATA_IN_METHOD')
  handler(): void {}
}

const foo = new Foo();

console.log(Reflect.getMetadata('class:key', Foo)); // METADATA_IN_CLASS

console.log(Reflect.getMetadata('method:key', Foo.prototype, 'prop')); // METADATA_IN_PROP
console.log(Reflect.getMetadata('prop:key', foo, 'prop')); // METADATA_IN_PROP

console.log(Reflect.getMetadata('method:key', Foo.prototype, 'handler')); // METADATA_IN_METHOD
console.log(Reflect.getMetadata('method:key', foo, 'handler')); // METADATA_IN_METHOD

# 控制反转与依赖注入

控制反转,是面向对象编程中的一种设计模式,可以用来很好地解耦代码。

假设存在多个具有关系的类:

代码语言:javascript复制
import { A } from './modA';
import { B } from './modB';

class C {
  constructor() {
    this.a = new A();
    this.b = new B();
  }
}

随着开发这些类的数量与依赖关系复杂度增加,C 依赖 A 、 B,D 依赖 A、C 等等,再加上每个类需要实例化的参数可能又有所不同,此时再去手动维护这些依赖关系与实例化过程就比较困难。

控制反转模式可以很好地解决这一问题,它引入了容器的概念,内部自动地维护这些类的依赖关系,当需要一个类时,它会帮助把这个类内部依赖的实例都填充好,然后开发者直接用就行:

代码语言:javascript复制
class F {
  constructor() {
    this.d = Container.get(D);
  }
}

此时,实例 D 已经完成了对 A、 C 的依赖填充,C 也完成了 A、B 的依赖填充。

这种维护依赖关系的模式,就是控制反转。之前手动维护关系的模式,是控制正转

控制反转的实现方式主要有两种,依赖查找依赖注入,其本质均是将依赖关系的维护与创建独立出来

# 依赖查找

依赖查找就是将实例化的过程放到了另一个新的 Factory 方法中:

代码语言:javascript复制
class Factory {
  static produce(key: string) {}
}

class F {
  constructor() {
    this.d = Factory.produce('D');
  }
}

Factory 类会按照传入的 key 去查找目标对象,然后再进行实例化与赋值过程。

# 依赖注入

代码语言:javascript复制
@Provide()
class F {
  @Inject()
  d: D;
}

Provide 标明这个类需要被注册到容器中,如果别的地方需要这个类 F 时,其内部的 d 属性需要被注入一个 D 的实例,而 D 实例又需要 AB 的实例等。这个系列的过程是完全交给容器的,开发者需要做的只是用装饰器简单标明下依赖关系即可。

装饰器通过元数据实现的依赖注入。

# 基于依赖注入的路由实现

代码语言:javascript复制
export enum METADAT_KEY {
  METHOD = 'ioc:method',
  PATH = 'ioc:path',
  MIDDLEWARE = 'ioc:middleware',
}

export enum REQUEST_METHOD {
  GET = 'ioc:get',
  POST = 'ioc:post',
}

export const methodDecoratorFactory = (method: string) => {
  return (path: string): MethodDecorator => {
    return (_target, _key, descriptor) => {
      // 在方法实现上注册 ioc:method 请求方法的元数据
      Reflect.defineMetadata(METADAT_KEY.METHOD, method, descriptor.value!);
      // 在方法实现上注册 ioc:path 请求路径的元数据
      Reflect.defineMetadata(METADAT_KEY.PATH, path, descriptor.value!);
    };
  };
};

export const Get = methodDecoratorFactory(REQUEST_METHOD.GET);
export const Post = methodDecoratorFactory(REQUEST_METHOD.POST);

此时 @Get('/list') 其实就是注册了 ioc:method - ioc:getioc:path - /list 的元数据,分别标识了请求方法与请求路径。

Controller 中,拿到请求路径信息,拼接在类的所有请求方法路径前:

代码语言:javascript复制
export const Controller = (path?: string): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(METADAT_KEY.PATH, path ?? '', target);
  };
};

在最后信息组装:

代码语言:javascript复制
type AsyncFunc = (...args: any[]) => Promise<any>;

interface ICollected {
  path: string;
  requestMedthod: string;
  requestHandler: AsyncFunc;
}

export const routerFactory = <T extends object>(ins: T): ICollected[] => {
  const prototype = Reflect.getPrototypeOf(ins) as any;

  const rootPath = <string>(Reflect.getMetadata(METADAT_KEY.PATH, prototype.constructor));

  const methods = <string[]>(
    Reflect.ownKeys(prototype).filter(item => item !== 'constructor')
  );

  const collected = methods.map((m) => {
    const requestHandler = prototype[m];
    const path = <string>Reflect.getMetadata(METADAT_KEY.PATH, requestHandler);

    const requestMedthod = <string>(
      Reflect.getMetadata(METADAT_KEY.METHOD, requestHandler).replace('ioc:', '')
    );

    return {
      path: rootPath   path,
      requestMedthod,
      requestHandler,
    };
  });

  return collected;
};

最后收集的信息:

代码语言:javascript复制
[
  {
    path: '/user/list',
    requestMedthod: 'get',
    requestHandler: [AsyncFunction: userList]
  },
  {
    path: '/user/add',
    requestMedthod: 'post',
    requestHandler: [AsyncFunction: userAdd]
  }
]

启动 HTTP 服务:

代码语言:javascript复制
import http from 'http';

http
  .createServer((req, res) => {
    for (const info of collected) {
      if (
        req.url === info.path &&
        req.method?.toLocaleLowerCase() === info.requestMedthod
      ) {
        info.requestHandler().then((data) => {
          res.writeHead(200, {
            'Content-Type': 'application/json',
          });
          res.end(data);
        });
      }
    }
  })
  .listen(3000)
  .on('listening', () => {
    console.log('server is listening on port 3000');
  });

Controller 中使用:

代码语言:javascript复制
@Controller('/user')
class UserController {
  @Get('/list')
  async userList() {
    return {
      success: true,
      code: 10000,
      data: [
        {
          name: '张三',
          age: 18,
        },
        {
          name: '李四',
          age: 20,
        },
      ],
    };
  }

  @Post('/add')
  async addUser() {
    return {
      success: true,
      code: 10000,
      data: null,
    };
  }
}

# 实现一个简易 IoC 容器

使用参数实现:

代码语言:javascript复制
type ClassStruct<T = any> = new (...args: any[]) => T;

class Container {
  private static services: Map<string, ClassStruct> = new Map();
  public static propertyRegistry: Map<string, string> = new Map();

  public static set(key: string, value: ClassStruct): void {
    Container.services.set(key, value);
  }

  public static get<T = any>(key: string): T | undefined {
    const Cons = Container.services.get(key);

    if (!Cons) {
      return undefined;
    }

    const instance = new Cons();

    for (const info of Container.propertyRegistry) {
      const [inject, serviceKey] = info;

      const [classKey, propKey] = inject.split(':');

      if (classKey !== Cons.name) {
        continue;
      }

      const service = Container.get(serviceKey);

      if (!service) {
        throw new Error(`service ${serviceKey} not found`);
      } else {
        instance[propKey] = service;
      }
    }

    return instance as T;
  }

  private constructor() {}
}

function Provide(key: string): ClassDecorator {
  return (Target) => {
    Container.set(key, Target as unknown as ClassStruct);
  };
}

function Inject(key: string): PropertyDecorator {
  return (target, propertyKey) => {
    Container.propertyRegistry.set(
      `${target.constructor.name}:${String(propertyKey)}`,
      key
    );
  };
}

测试:

代码语言:javascript复制
@Provide('DriverService')
class Driver {
  adapt(consumer: string) {
    console.log(`driver ${consumer} is running`);
  }
}

@Provide('Car')
class Car {
  @Inject('DriverService')
  private driver!: Driver;

  run() {
    this.driver.adapt('car');
  }
}

const car = Container.get<Car>('Car')!;

car.run();
//  "driver car is running" 

使用内置元数据进行优化:

代码语言:javascript复制
type ClassStruct<T = any> = new (...args: any[]) => T;
type ServiceKey<T = any> = string | ClassStruct<T> | Function;

class Container {
  private static services: Map<ServiceKey, ClassStruct> = new Map();
  public static propertyRegistry: Map<string, string> = new Map();

  public static set(key: ServiceKey, value: ClassStruct): void {
    Container.services.set(key, value);
  }

  public static get<T = any>(key: ServiceKey): T | undefined {
    const Cons = Container.services.get(key);

    if (!Cons) {
      return undefined;
    }

    const instance = new Cons();

    for (const info of Container.propertyRegistry) {
      const [inject, serviceKey] = info;

      const [classKey, propKey] = inject.split(':');

      if (classKey !== Cons.name) {
        continue;
      }

      const service = Container.get(serviceKey);

      if (!service) {
        throw new Error(`service ${serviceKey} not found`);
      } else {
        instance[propKey] = service;
      }
    }

    return instance as T;
  }

  private constructor() {}
}

function Provide(key?: string): ClassDecorator {
  return (Target) => {
    Container.set(key ?? Target.name, Target as unknown as ClassStruct);
    Container.set(Target, Target as unknown as ClassStruct)
  };
}

function Inject(key?: string): PropertyDecorator {
  return (target, propertyKey) => {
    Container.propertyRegistry.set(
      `${target.constructor.name}:${String(propertyKey)}`,
      key ?? Reflect.getMetadata('design:type', target, propertyKey)
    );
  };
}

测试:

代码语言:javascript复制
@Provide()
class Driver {
  adapt(consumer: string) {
    console.log(`driver ${consumer} is running`);
  }
}

@Provide()
class Car {
  @Inject()
  private driver!: Driver;

  run() {
    this.driver.adapt('car');
  }
}

const car = Container.get(Car)!;
car.run();
//  "driver car is running" 

0 人点赞