用reflect-metadata和装饰器实现依赖注入和控制反转
# 问题的由来
无论是NestJS或者MidwayJS,它们都用到了依赖注入和控制反转来实现OOP编程,而他们几乎都是通过装饰器和元数据来实现的。说到这里很多人很懵,我在这里举个NestJS中常见的日常开发例子:
@Injectable()
class CatService {
constructor(public readonly animalService: AnimalService) {}
run(){
this.animalService.run()
}
}
有没有想过这个animalService是通过什么原理自动实例成AnimalService的呢?还有控制器:
@Controller('cats')
export class CatsController {
constructor(public readonly catService: CatService) {}
@Get('/run')
run(){
this.catService.run()
}
}
你能大概猜到他们是怎么直接通过装饰器@Controller 和 @Get 就把CatsController的run方法应用在了 “/casts/run” 路由上吗?
# 基础知识
# 类的装饰
我们这里直接饮用阮一峰的一段代码:
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
我们可以看出可以通过装饰器函数的函数 target 拿到类本身。
# 方法的装饰
首先我们先来温习下装饰器的一些基础知识,首先是方法装饰器,这里再引用一段代码:
class Math {
@log
add(a, b) {
return a + b;
}
}
function log(target, name, descriptor) {
var oldValue = descriptor.value;
descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(this, arguments);
};
return descriptor;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);
如上代码,方法的装饰器函数可以直接重定义所装饰的方法的内容,也就是说他可以直接用第三个参数 descriptor 拿到要装饰方法本身。
# reflect-metadata
元数据,本身就是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。Typescript1.5+ 就已经支持了,我们可以通过npm直接安装
npm i reflect-metadata --save
然后在 tsconfig.json 里配置 emitDecoratorMetadata 选项:
{
"compilerOptions": {
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
我们可以先看下 reflect-metadata 的基本用法:
@Reflect.metadata('inClass', 'A')
class Test {
@Reflect.metadata('inMethod', 'B')
public hello(): string {
return 'hello world';
}
}
console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'
可以看到 Reflect.metadata当作装饰器用的时候,可以修饰类的时候添加元数据,修饰方法的时候也可以往方法上添加元数据。最关键的是它可以通过 Reflect.getMetadata("design:paramtypes", target, key)获取函数参数类型。
# 自定义 key
reflect-metadata 除了可以获取类型的信息还能直接自定义 key 存储数据,比如:
function classDecorator(): ClassDecorator {
return target => {
// 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
Reflect.defineMetadata('classMetaData', 'a', target);
};
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
Reflect.defineMetadata('methodMetaData', 'b', target, key);
};
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
}
Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
# 实现
# 控制反转
我们找到上面的代码增加控制反转工厂函数:
@Injectable()
class CatService {
constructor(public readonly animalService: AnimalService) {}
run(){
this.animalService.run()
}
}
const factory = <T>(target: Constructor<T>): T => {
const providers = Reflect.getMetadata('design:paramtypes', target); // [AnimalService]
const args = providers.map((provider: Constructor) => new provider()); // 全部实例化
return new target(...args);
};
factory(CatService).run();
通过 Reflect.getMetadata('design:paramtypes', target)
# @Controller 和 @Get的实现
我们这里直接借用下《深入浅出 Typescript》的一段代码:
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
// @Controller 传进来的参数存储值,看不懂可以往上去看“自定义 key”的描述
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
// 分别存储请求的方法和路径,记住 descriptor.value,你现在可以上去看文章前面的“方法装饰”
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
接着实现 Route 的 映射:
function mapRoute(instance: Object) {
const prototype = Object.getPrototypeOf(instance);
// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]));
return methodsNames.map(methodName => {
// fn 就是前面的 descriptor.value
const fn = prototype[methodName];
// 取出所有定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
};
这时候我们把 mapRoute遍历然后把相关挂载在 express 或者 koa 上就可以了,其实《深入浅出 Typescript》这本书的 Reflect Metadata 章节里面有讲过这个的实现,但是很多 Typescript 基础不够牢固的同学可能无法理解,知识本身就是连贯性的,一层一层深入,不过难免有时候某些内容不够牢固导致会卡壳,这时候就需要回去补下对应的知识才能走下去。共勉!!!