# 装饰器与反射元数据
# 装饰器
装饰器的本质是一个函数,只不过它的入参时提前确定好的。TypeScript 中的装饰器目前只能在类及类成员上使用。
代码语言:javascript复制function Deco() {}
@Deco
class Foo {}
实际使用中更多的是装饰器工厂:
代码语言:javascript复制function Deco() {
return () => {}
}
@Deco()
class Foo {}
// 程序执行时会先执行 Deco(),再用内部返回的函数作为装饰器的实际逻辑
// 以此可以通过入参来灵活调整装饰器的作用
TypeScript 中的装饰器可以分为:类装饰器、方法装饰器、访问符装饰器、属性装饰器和参数装饰器。
- 类装饰器
- 直接作用在类上的装饰器
- 执行时的入参只有一个,即被装饰的类
- 可以通过类装饰器来覆盖类的属性和方法,如果在类装饰器中返回一个新的类,甚至可以篡改整个类的实现
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
- 方法装饰器
- 入参包括类的原型,方法名和方法的属性描述符
- 通过属性描述符可以控制这个方法的内部实现、可变性等
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
时触发,setter
在value
被赋值时触发- 访问器本质还是方法装饰器
- 注意,访问符装饰器只能同时应用在一对
getter/setter
的其中一个,因为不论装饰哪一个,装饰器入参的属性描述符都会包括getter
和setter
方法
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
- 属性装饰器
- 属性装饰器在独立使用时能力非常有限
- 其入参只有类的原型和属性名称,返回值会被忽略
- 但是,仍然可以通过直接在类的原型上赋值来修改属性
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
- 参数装饰器
- 参数装饰器包括了构造函数的参数装饰器和方法的参数装饰器
- 其入参包括类的原型、参数所在的方法名与参数在函数中的索引值(即第几个参数)
- 在单独使用时,作用也比较有限
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 官方文档中对应顺序给出了详细的定义:
- 参数装饰器,然后依次是方法装饰器、访问符装饰器或属性装饰器应用到每个实例成员
- 参数装饰器,然后依次是方法装饰器、访问符装饰器或属性装饰器应用到每个静态成员
- 参数装饰器应用到构造函数
- 类装饰器应用到类
# 反射 Reflect
Reflect 在 ES6 中首次引入,主要是为了配合 Proxy
保留一份方法原始的实现逻辑:
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.defineMetadata
、Reflect.getMetadata
等。可以将元数据理解为用于描述数据的数据,如某个方法的参数信息、返回值信息就可以称为该方法的元数据。
为类或类属性添加元数据后,构造函数会具有 [[Metadata]]
属性,该属性内部包含一个 Map
结构,键为属性键,值为元数据键值对。静态成员的元数据信息存储于构造函数,而实例成员的元数据信息存储与构造函数的原型上。
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
方法中:
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
实例又需要 A
、B
的实例等。这个系列的过程是完全交给容器的,开发者需要做的只是用装饰器简单标明下依赖关系即可。
装饰器通过元数据实现的依赖注入。
# 基于依赖注入的路由实现
代码语言: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:get
与 ioc:path - /list
的元数据,分别标识了请求方法与请求路径。
Controller
中,拿到请求路径信息,拼接在类的所有请求方法路径前:
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
中使用:
@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"