前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TS 进阶 - 实际应用 03

TS 进阶 - 实际应用 03

作者头像
Cellinlab
发布2023-05-17 20:42:04
4570
发布2023-05-17 20:42:04
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 装饰器与反射元数据

# 装饰器

装饰器的本质是一个函数,只不过它的入参时提前确定好的。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" 
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022/8/11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 装饰器与反射元数据
    • # 装饰器
      • # 装饰器的执行机制
        • # 反射 Reflect
          • # 反射元数据 Reflect Metadata
          • # 控制反转与依赖注入
            • # 依赖查找
              • # 依赖注入
                • # 基于依赖注入的路由实现
                  • # 实现一个简易 IoC 容器
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档