typescript 高级应用 - 18 行代码实现依赖注入框架

541 阅读2分钟

前言

依赖注入在后端领域一直是理所当然, 而在前端则一直有一种水土不服的感觉, 确实没有什么场景是非依赖注入不可的.

先看成果, 真的只有 18 行. 好吧, 标题党了, 18 行只能当POC.

const registeredTargets = new Map();
export function resolve(target) {
    if (registeredTargets.has(target)) return registeredTargets.get(target);
    const instance = construct(target, typeinfo.get(target));
    registeredTargets.set(target, instance);
    return instance;
}
export function construct(target, params) {
    return new target(...params.map(e => resolve(e)));
}
export const typeinfo = new Map();
    
export function injectable() {
    return function(target) {
        const params: any[] = Reflect.getMetadata('design:paramtypes', target) || [];
        typeinfo.set(target, params);
    }
}

使用例子:

下面代码可以看到, Foo 依赖 Database, 对 Database 加上@Injectable 装饰器后, Foo 在使用 Database 这个类时只需要在 constructor 中声明即可, 不需要重新 new Database(), 实现 Foo 与 Database 解耦. 需要使用 Foo 时, 直接调用 resolve 即可返回 Foo 的实例.

stackblitz.com/edit/typesc…

// di-main.ts
// entry
import 'reflect-metadata';
import  {resolve} from '../src/index';
import { Foo } from './di-foo';
    
const instance = resolve(Foo);
console.log(instance.foo()); // output: foo database
// di-foo.ts
// entry resolves this class
import {injectable} from "../src/index";
import { DataBase } from './di-database';
    
@injectable()
export class Foo {
  constructor(private database: DataBase) {}
  foo() {
      this.database.foo();
  }
}
// di-database.ts
// Foo resolve Database
import {injectable} from "../src/index";
    
@injectable()
export class DataBase {
    foo() {
        console.log('foo database');
    }
}

原理

关键问题在于, 如何优雅地记录依赖信息, 如果每个类的依赖信息需要用户自行记录, 这个框架是完全不可用的, 我们希望得到在编译器层面的支持.

typescript 1.5 开始就支持 metadata(感谢 ts!), 即 ts 会在编译的时候将编译的得到的元信息返回出来使用, 通过 Reflect.getMetadata('design:paramtypes', target) 就就可以获取装饰器所装饰类的入参. 比如:

@injectable()
export class Foo {
  constructor(private database: DataBase) {}
}

这里 Reflect.getMetadata('design:paramtypes', target) 返回 [DataBase], DataBase是构造函数.

需要使用 metadata 的前提是:

1 tsconfig.json 需要加入以下配置, 表示编译时输出 metadata

{
    "compilerOptions": {
				"experimentalDecorators": true,
        "emitDecoratorMetadata": true,
    },
}

2 添加 meta-data 的 polyfill

    npm install --dev reflect-metadata

这样, 我们可以在装饰器中将类的入参保存起来, 后面实例化某个类时就可以通过递归的方式实例化其全部依赖. 后面就是单纯的逻辑问题了.