依赖注入概述
使用过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
标记可注入的服务,在Module
和Component
中提供可被注入的服务,然后在组件中使用这些服务。提供在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
静态方法可以创建一个新的Injector
,get
成员方法可以提供该注入器上的服务实例,具体使用:
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": []
}
]
}
可以看到清晰的注入器父子关系。