前面我们已经了解了Typescript中的装饰器, 我们知道了装饰器可以帮我们完成一些简单的操作, 但是, 这显然只是装饰器的一部分能力, 如果仅有这部分能力, 它根本不值得我们去学习; 所以, 本节我们要学习的一个很重要的概念: 控制反转和依赖注入
控制反转概念
控制反转, 英文名Inversion of Control, 即IOC何为控制反转? 先看看以下案例
class Feul {}
class Car {
feul: Feul | undefined;
run() {
if (this.feul) {
console.log('success! I can run!');
} else {
console.log('sorry, I can not run')
}
}
}
const car = new Car()
car.run() // sorry, I can not run
从以上案例可以知道, Car(汽车), 依赖于feul(汽油), 车有油了才能跑, 不给Car的feul属性赋值, 'Car'是run不起来的, 此时 怎么办? 简单, 我们可以给car'加油'
car.feul = new Feul('95#') // 给加95
但是如果Feul也有依赖呢? 比如, 必须依赖于有加油站, 或者说咱手头要有钱, 那怎么办? 我们就继续给Feul设置依赖条件?
Feul.isHasMoney = true
Feul.isHasStation = true
// ....也许还有
那如果依赖继续增加呢? 想要加油还必须有身份证, 那怎么办? 或者连我们的钱都规定了必须是现金! 那我们一个个加? 如果依赖项非常多, 层级非常深, 这显然是无法维护的; 所以, 我们必须要把这个反复设置依赖项的控制权交给程序, 让它来完成, 这种就叫控制反转, 也可以说是, 我们把控制权, 说白了就是干累活的'权利', 交给程序, 而不是我们自己手动来一个个抠! 所以, 我们希望能有一个容器(Container)帮我们处理好所有的依赖项和依赖关系:
const car = container.get(Car)
car.run() // 'success! I can run!'
控制反转终究只是一种设计原则, 即指导思想, 真正去实现, 还要依靠依赖查找和依赖注入;
依赖查找
关于依赖查找, 其实就是将依赖项的查找和实例化、赋值, 都放进一个类的方法中去执行, 通常需要传入一个key作为查找的依据
class Factory {
static create(key) {
// todo
}
}
class Car {
feul: Feul | undefined;
run() {
if (this.feul) {
console.log('I have feul, so I can run');
}
}
constructor() {
this.feul = Factory.create('key');
}
}
这样, 只要我们new Car(), feul属性就会被自动赋值
依赖注入
前面的依赖查找, 我们需要在每个类上增加一个额外的方法, 虽然也没多少代码, 但是偷懒是程序员的追求之一; 而依赖注入则更加简洁, 使用依赖注入, 我们只需要将各种依赖关系用装饰器标注清楚就行了, 而这正是装饰器的另一种重要用途;
@Provide()
class Car {
@Inject()
feul: Feul | undefined;
}
Provide装饰器负责将Car注册到容器中, Inject则是表示Car依赖于feul属性的值; 通过这种方式, 就能够确定依赖关系了, 之前说过, 元数据是描述数据的数据, 同样, 它也能描述一个属性需要的依赖项, 等到要执行的时候, 直接读取其元数据, 就能知道它的依赖关系了; 那么如何实现呢? 如何将描述好的关系, 转为实际的逻辑呢? 我们需要一个IOC容器
案例1, IOC容器
前面已经说了, 我们想要的效果就是, 写几个装饰器确定下依赖关系, 然后丢进一个容器就完事了:
@Provide()
class Car {
@Inject()
feul: Feul | undefined;
}
const car = container.get(Car)
car.run() // 'success! I can run!'
那么怎么实现这个控制反转(IOC)容器? 我们来整理下思路:
- 我们要有一个映射表, 里面存着所有注册了的类, 这个任务交给provide装饰器.
- 我们要将这个类所依赖的属性的类型也记录下来, 也存进一个映射表, 这个任务自然是交给Inject, 不过这个表要稍微复杂些, 里面必须包含这个属性属于哪个类, 以及这个属性需要的值是什么类型等信息
- 以上两个表建立起来后, 我们还要在get的时候, 查到注册表中的类(provide建立的表), 然后通过关系表(inject建立的表), 找出这个类所以依赖的属性以及它所能接受的类; 再从provide的映射表中找出这个类, 将其实例化后赋给这个属性!
好了, 按照这套逻辑, 我们来写代码, 首先, 我们的provide要负责建立映射表, 既然是映射表, 那肯定需要有键值, 值自然是其所修饰的类, 键呢? 可以是这个类的name, 也可以是用户传给provide的一个自定义键, 一个字符串
function Provide(key?: string): ClassDecorator {
return function (target: any) {
Container.set(key ?? target.name, target);
Container.set(target, target);
};
}
这里为什么要set一个target呢? 即 以整个类为一个键, 别忘了, 我们前面写的期望代码是这样的
const car = container.get(Car)
就是我哪怕传一个类作为键, 它也能从映射表中找到对应的类, 这是最简便的; 那如果我们不传类, 我们就想自己定义这个映射表的键名, 那就可以传一个字符串;
@Provide('myCar')
class Car {
// ...
}
const car = Container.get('myCar');
所以我们又考虑了provide参数key存在的情况, 如果key不存在(undefined || null), 我们就取类的name; 这样写是为了兼容更多写法; 此时我们的映射表就有这些情况
Car -> Car
Car.name -> Car
// or
'myCar' -> Car
我们再来看看inject, 前面说了, inject必须负责建立关系映射表
function Inject(key?: string): PropertyDecorator {
return function (prototype: any, propName: string | symbol) {
Container.regsiterProperty(
`${prototype.constructor.name}:${String(propName)}`,
key ?? Reflect.getMetadata('design:type', prototype, propName)
);
};
}
注意了, 'design:type'为内置元数据, 可以获取其修饰的属性, 所需要的类, 我们可以看到, 关系映射表结构是这样的:
className:propName -> 该属性需要接受的类
className:propName -> 自定义的键
也就是, 类:类的属性 -> 属性需要的类 或者 自定义的键; 这里要注意的是这个映射表的值, 指的就是上一个映射表中的键! 即 我们通过类和属性这两个维度, 确定上一个映射表的键, 从而找到这个属性需要的那个类, 然后, 我们再实例化这个类, 将实例赋值给这个属性;
好了, 讲完了这两个装饰器, 我们要来看看Container里的逻辑该怎么实现了, 前面的代码中, 我们发现Container有set方法、regsiterProperty方法、get方法, 先来看看set方法, 我们说了好多次第一个映射表, 现在看看它到底怎么实现吧
type classInstance<T = any> = new (...args: any[]) => T;
type ServiceKey<T = any> = string | classInstance<T>;
class Container {
// provide维护的映射表
private static services: Map<ServiceKey, classInstance> = new Map();
static set(key: ServiceKey, value: classInstance) {
Container.services.set(key, value);
}
// ...
}
前面已经解释过了, 第一个映射表的键, 可以是一个类, 也可以是一个字符串格式的自定义键, 所以, 我们索性将其封装为ServiceKey类型
再看看inject的关系映射表, 我们使用了regsiterProperty方法维护它
class Container {
private static propertyRegisters: Map<string, ServiceKey> = new Map();
static regsiterProperty(key: string, value: ServiceKey) {
Container.propertyRegisters.set(key, value);
}
// ...
}
这个关系映射表的值就是上一个映射表的键, 所以, 也干脆定义为ServiceKey;
最后, 也是最重要的get方法, 它负责将以上逻辑都整合起来, 它需要通过传入的一个参数, 从第一个映射表中找到对应的类, 然后从第二张关系映射表里找到它所有属性的依赖类, 再将其实例化赋给属性;
class Container {
static get(key: ServiceKey): any {
// 从第一张映射表中找到对应的类
const Cons = Container.services.get(key);
// 找不到就直接return了
if (!Cons) return;
// 找到则实例化
const ins = new Cons();
// 获取关系表
const Relations = Container.propertyRegisters;
for (const item of Relations) {
// key就是字符串, 格式为 '类名:属性名'
// value就是ServiceKey类型
const [key, value] = item;
// 类名
const name = key.split(':')[0];
// 对比关系映射表的中的类名, 和获取到的类名; 如果不等就直接跳过
if (Cons.name !== name) continue;
// 找到对应类的属性名
const propKey = key.split(':')[1];
// 将属性所需要的那个类传入get,递归
const result = Container.get(value);
if (result) {
// 赋值给对应的属性
ins[propKey] = result;
}
}
// 返回
return ins;
}
// ...
}
好了, 我们来看看完整代码
import 'reflect-metadata';
type classInstance<T = any> = new (...args: any[]) => T;
type ServiceKey<T = any> = string | classInstance<T>;
class Container {
private static services: Map<ServiceKey, classInstance> = new Map();
static set(key: ServiceKey, value: classInstance) {
Container.services.set(key, value);
}
private static propertyRegisters: Map<string, ServiceKey> = new Map();
static regsiterProperty(key: string, value: ServiceKey) {
Container.propertyRegisters.set(key, value);
}
static get(key: ServiceKey): any {
const Cons = Container.services.get(key);
if (!Cons) return;
const ins = new Cons();
const Relations = Container.propertyRegisters;
for (const item of Relations) {
const [key, value] = item;
const name = key.split(':')[0];
if (Cons.name !== name) continue;
const propKey = key.split(':')[1];
const result = Container.get(value);
if (result) {
ins[propKey] = result;
}
}
return ins;
}
}
function Provide(key?: string): ClassDecorator {
return function (target: any) {
Container.set(key ?? target.name, target);
Container.set(target, target);
};
}
function Inject(key?: string): PropertyDecorator {
return function (prototype: any, propName: string | symbol) {
Container.regsiterProperty(
`${prototype.constructor.name}:${String(propName)}`,
key ?? Reflect.getMetadata('design:type', prototype, propName)
);
};
}
来试试效果如何吧
@Provide('汽油')
class Feul {
NO: string = '95#';
}
@Provide()
class Car {
@Inject('汽油')
feul!: Feul;
constructor() {}
run() {
console.log(this.feul.NO);
}
}
const car = Container.get(Car);
car.run(); // 95#
案例2, 基于依赖注入的路由
在很多nodeJS的库中, 例如: Nest中就大量使用了装饰器, 例如, 使用装饰器来实现一个路由
@Controller('/public')
class Request {
@Get('/lists')
getLists() {
return new Promise((resolve) => {
resolve('this is lists');
});
}
@Post('/push')
push() {
return new Promise((resolve) => {
resolve('this is push');
});
}
}
即, 路径是一个依赖, 将依赖以参数的形式, 注入到装饰器中, 只有依赖条件都符合了, 即请求路径对了, 才会执行到对应的方法, 我们来看看如何实现它吧
import 'reflect-metadata';
// 请求方法/请求路径元数据的键
enum KEYS {
MethodKey = 'ioc:method',
PathKey = 'ioc:path',
}
// 请求方法
enum METHODS {
GET = 'get',
POST = 'post',
}
// methodDecorate, 一个偏函数
export function methodDecorate(method: METHODS) {
return function (path: string): MethodDecorator {
return function (_target, _methodName, descriptor) {
Reflect.defineMetadata(KEYS.MethodKey, method, descriptor.value!);
Reflect.defineMetadata(KEYS.PathKey, path, descriptor.value!);
};
};
}
// 确定请求方法, 获得Get/Post装饰器
export const Get = methodDecorate(METHODS.GET);
export const Post = methodDecorate(METHODS.POST);
// 根路径装饰器
export function Controller(rootPath: string): ClassDecorator {
return function (target: any) {
Reflect.defineMetadata(KEYS.PathKey, rootPath, target);
};
}
以上我们先通过methodDecorate这个偏函数, 确定了请求方法, 得到Get和Post这俩方法装饰器, 而装饰器内部, 其实就是将请求方法和请求路径注册为元数据, 挂到方法实体(descriptor.value)上; 然后定义Controller类装饰器, 将根路径注册为类的元数据; 我们先写一个简单的class试试, 看看装饰器是否都能成功注册数据
@Controller('/root')
class Foo {
@Get('/info')
getInfo() {}
@Post('/push')
upload() {}
}
const foo = new Foo();
console.log(Reflect.getMetadata(KEYS.MethodKey, foo.getInfo)); // get
console.log(Reflect.getMetadata(KEYS.PathKey, foo.getInfo)); // /info
console.log(Reflect.getMetadata(KEYS.MethodKey, foo.upload)); // post
console.log(Reflect.getMetadata(KEYS.PathKey, foo.upload)); // /push
接下来, 我们要做的事情就是, 需要有一个方法, 将这些信息获取并整合
// 我们希望拿到的数据
interface ReqInfo {
requestMethod: METHODS;
requestPath: string;
requestHandler: (...args: any[]) => Promise<void>;
}
// 整合元数据的方法
export function routerFactory(target: any): ReqInfo[] {
// 获取原型对象
const prototype = Reflect.getPrototypeOf(target) as any;
// 获取根路径
const rootPath = Reflect.getMetadata(KEYS.PathKey, prototype.constructor);
// 获取实例的方法
const methodKeys = Reflect.ownKeys(prototype).filter(
(key) => key !== 'constructor'
);
let result: any[] = [];
methodKeys.forEach((methodKey) => {
// 获取执行方法
const requestHandler = prototype[methodKey];
// 请求路径
const requestPath = Reflect.getMetadata(KEYS.PathKey, requestHandler);
// 请求方法
const requestMethod = Reflect.getMetadata(KEYS.MethodKey, requestHandler);
result.push({
requestMethod,
requestPath: rootPath + requestPath,
requestHandler: requestHandler,
});
});
return result;
}
现在来用用我们写好的装饰器和组装函数吧
@Controller('/public')
class Request {
@Get('/lists')
getLists() {
return new Promise((resolve) => {
resolve('this is lists');
});
}
@Post('/push')
push() {
return new Promise((resolve) => {
resolve('this is push');
});
}
}
const request = new Request();
const arr = routerFactory(request);
// arr数据如下
/**
* [
{
requestMethod: 'get',
requestPath: '/public/lists',
requestHandler: [Function: getLists]
},
{
requestMethod: 'post',
requestPath: '/public/push',
requestHandler: [Function: push]
}
]
*/
我们获得了数组arr, 这样, 就可以根据不同的请求, 执行对应的方法了! 我们来搭建个简单的http服务:
import http from 'http';
http
.createServer((req, res) => {
// 遍历arr,判断其请求方法/路径是否匹配
for (let item of arr) {
if (
req.url === item.requestPath &&
req.method === item.requestMethod.toUpperCase()
) {
// 匹配则执行方法
item.requestHandler().then((result) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(result);
});
}
}
})
.listen(3000)
.on('listening', () => {
console.log('监听成功');
});
来看看实际页面的结果
这样, 我们就实现了一个基于依赖注入的路由了, 我们只需要在一个类上定义装饰器, 就能实现路由功能了!