依赖注入概述

使用过Angular的读者一定对依赖注入不陌生。从Angular.js到Angular,依赖注入都是绕不开的话题。在其他语言或者框架下,依赖注入也广泛应用,比如JAVA/SCALA使用Guice,C#使用Ninject。什么是依赖注入?简而言之,就是当前某个逻辑模块获取它依赖的服务且不控制服务的生命周期(创建销毁等)。例如服务a依赖服务b,但是a并不需要创建服务b就可以获得服务b的实例,我们把服务a获得服务b实例的过程叫做依赖注入。Angular中,通用的过程是在constructor中定义依赖服务的类型即可。

本文目标

怎么样实现依赖注入?通常的逻辑是会有依赖注入容器处理依赖注入,把依赖的服务被依赖之前创建完成,并注入到被依赖的逻辑模块中。Guice中可把服务定义为Eager,在程序启动时就会初始化服务;也可将服务按需加载,服务实例会存在内存的一块区域,当被依赖时就会取出使用。在Typescript/Javascript社区也有非常多优秀的依赖注入库,比如Inversify(https://github.com/inversify/InversifyJS)。这篇文章的目的并不是要再造一个大而全的轮子,而是通过使用Typescript提供的高级特性,演绎一个简单的依赖注入逻辑,加深对依赖注入的理解。

我们的切入点和参照系还是以Angular为主,Angular中我们主要使用依赖注入的方式是通过Injectable标记可注入的服务,在ModuleComponent中提供可被注入的服务,然后在组件中使用这些服务。提供在Component中的服务和父级组件和模块中相同的服务是隔离的,从而提供组件层面更灵活的操作,如果组件内依赖的服务没有定义在组件层级,则会往父层级寻找。由此,我们可以归纳几点需求

  • 实现提供类型得到实例的依赖注入模型. getInstance(type: Constructor<SomeClass>): SomeClass
  • 实现隔离的服务层级模块.

如何实现

代码实现发布在 https://github.com/kingfolk/dilight ,代码Demo外部框架使用Angular,具体注入部分逻辑代码在projects/lib下。虽然具体实现只有200行左右,但仍然占据比较大的篇幅,可访问 http://dilight.surge.sh/ 看使用方法,了解实现之后的使用从而有一定使用的概念,可以帮助理解本文依赖注入实现。

工具:装饰器和反射

装饰器可以参看之前的一篇文章 https://zhuanlan.zhihu.com/p/56596588 。我们仿照Angular定义两个装饰器

  • Injectable 定义可被注入的服务
  • Module 定义独立服务的提供商

对反射比较陌生的读者可以参看 https://rbuckton.github.io/reflect-metadata/ 、 https://www.jianshu.com/p/653bce04db0b 。简而言之反射可以帮助我们在js运行时得到对象身上额外的信息。这里反射帮助我们拿到了类构建函数的参数类型信息。

装饰器使用如下:


// 一个可被注入的服务
@Injectable()
class ServiceA {}

// 一个隔离的服务提供商
@Module({
  providers: [ServiceA]
})
class ServiceB {
  constructor(private a: ServiceA) {}
}



Injectable实现



export interface Type<T> {
  new(...args: any[]): T;
}

export function Injectable() {
  return InjectableConstructor;
}

export function InjectableConstructor<T>(target: Type<T>) {
  const types = Reflect.getMetadata('design:paramtypes', target);
  if (types) {
    const paramPrototypes = types.map((type) => type);
    Reflect.defineMetadata('inject:target:constructor', paramPrototypes, target);
  }
}



Type接口表示的是类类型。装饰器用在ServiceA之上,target就是ServiceA的构造函数。Reflect.getMetadata('design:paramtypes', target);得到构造函数的参数,并且我们把这些参数记录在inject:target:constructor这个反射key上,需要的时候使用。

Module实现



export class InjectorParams {
  providers: Type<any>[];
}

export function Module(params?: InjectorParams) {
  return function <T> (target: Type<T>) {
    Reflect.defineMetadata('inject:target:injector', true, target);
    InjectableConstructor(target);
    if (params && params.providers) {
      Reflect.defineMetadata('inject:target:providers', providers, target);
    }
  };
}



Module可定义一个providers服务数组,用来定义该Module上的注入器有哪些服务可以提供,记录在inject:target:providers这个key上。并且对反射keyinject:target:injector设为true,表示该类为模块类,在之后的注入器初始化服务时判断模块类并做一些不一样的处理。

注入器实现

我们这次实现的主要逻辑就是Angular的Injector服务。在开发Angular应用时,大多数情况下都不需要使用Injector服务,除非某些服务不能通过构建时注入得到,我们可以通过在组件中注入Injector,然后再稍后调用get方法得到服务。Angular中每个组件和模块都对应各自的Injector,当组件初始化需提供依赖的服务时,便会在组件层面的Injector中寻找,如果找不到则往父级查找。更多Angular层级Injector的内容可以前往 https://angular.io/guide/hierarchical-dependency-injection。

我们可以观察一下Angular Injector的接口定义



abstract class Injector {
  static THROW_IF_NOT_FOUND: _THROW_IF_NOT_FOUND
  static NULL: Injector
  static ngInjectableDef: defineInjectable({...})
  static create(options: StaticProvider[] | { providers: StaticProvider[]; parent?: Injector; name?: string; }, parent?: Injector): Injector
  abstract get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T
}



这边我们只需关注create静态方法和get成员方法。create静态方法可以创建一个新的Injectorget成员方法可以提供该注入器上的服务实例,具体使用:



const injector: Injector =
    Injector.create({providers: [{provide: 'validToken', useValue: 'Value'}]});
expect(injector.get('validToken')).toEqual('Value');
expect(() => injector.get('invalidToken')).toThrowError();
expect(injector.get('invalidToken', 'notFound')).toEqual('notFound');



此处的Injector接口定义为虚类,我们需要定义额外的成员和方法才能实现简单的注入逻辑。



export class Injector {
  static INJECTOR_ID = 0;

  private providers = new Set<Type<any>>();

  private instances = new Map<Type<any>, Object>();

  id = Injector.INJECTOR_ID ++;

  readonly parent: Injector;

  static create(providers: Type<any>[] = [], parentInjector?: Injector) {
    const injector = new Injector(parentInjector);
    providers.forEach((p) => {
      injector.addProvider(p);
    });
    return injector;
  }

  constructor(parentInjector?: Injector) {
    this.providers.add(Injector);
    this.instances.set(Injector, this);
    if (parentInjector) {
      this.parent = parentInjector;
    }
  }

  get<T>(provider: Type<T>): T {}
}



通过调用Injector.create(providers, parentInjector)我们得到新的Injector实例。providers成员为该注入器所有提供的服务,instances为该注入器所有的服务实例。get方法根据服务类型返回服务实例。在Injector构造函数中,把Injector服务本身假如到提供商集合中,并将自身放入服务实例映射中,这样在其他服务中就可注入Injector服务,这样在运行中自由地注入所需要的服务。



get<T>(provider: Type<T>): T {
    if (!this.hasOwnProvider(provider) && this.parent) {
      return this.parent.get(provider);
    }  else {
      return this.getInstance(provider);
    }
  }

  private hasOwnProvider<T>(provider: Type<T>) {
    return this.providers.has(provider);
  }



对于在该注入器里提供的服务,调用getInstance;对于没有提供的服务,对parent父注入器递归调用get方法。



private getInstance<T>(provider: Type<T>): T {
    if (this.instances.has(provider)) {
      return this.instances.get(provider) as T;
    }

    let instance: T;
    const isModule = Reflect.getMetadata('inject:target:injector', provider);
    if (isModule) {
      const providers = Reflect.getMetadata('inject:target:providers', target) || [];
      const injector = Injector.create(providers, this);
      instance = injector.createInstance(target);
    } else {
      instance = this.createInstance(provider);
    }
    this.instances.set(provider, instance);

    return instance;
  }



isModule根据之前Module装饰器存储的模块信息判断,如果是模块,执行以下代码:



// 根据Module装饰器得到providers列表
  const providers = Reflect.getMetadata('inject:target:providers', target) || [];
  // 新建一个Injector,并建立父子关系
  const injector = Injector.create(providers, this);
  // 实例化所需服务
  instance = injector.createInstance(target);



createInstance实现如下:



private createInstance<T>(provider: Type<T>): T {
    const constructorParams = Reflect.getMetadata('inject:target:constructor', provider) || [];
    // If constructorParams meta does not meet with provider's constructor params. then throw error.
    if (constructorParams.length !== provider.length) {
      throw Error(`class ${provider.name} is not injectable`);
    }
    const paramInstances = constructorParams.map((paramProvider) => this.get(paramProvider));
    const hostInstance = new provider(...paramInstances);

    return hostInstance;
  }



createInstance方法根据之前存储的inject:target:constructor得到类构造函数参数列表。constructorParams.length !== provider.length判断该服务是否可以被注入,如果缺失了构造函数参数类信息,则注入无法继续并抛出异常(假如服务A依赖服务B并注入服务B,但是服务A并未在任何地方提供,那么注入服务A逻辑上无法达到)。具体代码示例



const paramInstances = constructorParams.map((paramProvider) => this.get(paramProvider));
  const hostInstance = new provider(...paramInstances);



对所有参数服务递归地得到服务实例,并最后返回provider实例。

其余实现

抽象一个全局注入的工厂类。



export class Dilight {
  static new(provider: Type<any>[] = [], parentInjector?: Injector) {
    return Injector.create(provider, parentInjector);
  }
}



用于快速查看某个Injector的状态,包括服务、实例等。



snapshot() {
    const providers = Array.from(this.providers).map((provider) => provider.name);
    const instances = Array.from(this.instances.keys()).map((provider) => provider.name);
    const children = this.children.map((c) => c.snapshot());
    return {injectorId: this.id, providers, instances, children};
  }



使用

使用示列可访问http://dilight.surge.sh/。



import {Dilight, Injectable, Module, Injector} from 'dilight';

@Injectable()
class ServiceA {}

@Injectable()
class ServiceB {
  constructor(private a: ServiceA) {}
}

@Module({
  providers: [ServiceB],
})
class SomeComponent {
  constructor(public childInjector: Injector) {}
}

const parentInjector = Dilight.new();
const comp = parentInjector.get(SomeComponent);


// will log false
console.log(comp.childInjector.get(ServiceB) === parentInjector.get(ServiceB));

// will log true
console.log(comp.childInjector.get(ServiceA) === parentInjector.get(ServiceA));



我们调用parentInjector.snapshot(),可以看到如下信息:



{
  "injectorId": 0,
  "providers": [
    "Injector"
  ],
  "instances": [
    "Injector",
    "SomeComponent"
  ],
  "children": [
    {
      "injectorId": 1,
      "providers": [
        "Injector",
        "ServiceB"
      ],
      "instances": [
        "Injector"
      ],
      "children": []
    }
  ]
}



可以看到清晰的注入器父子关系。