阅读本文前,请熟练掌握以下最最基本概念: Reflect 修饰器 * Ioc
对应的几篇小文章可以参考: JS的反射学习和应用 AOP和IoC在点我达前端的实践
1、基础概念
学习inversify之前,我们需要了解下Ioc的一些概念,建议阅读一下之前写过的一篇文章:AOP和IoC在点我达前端的实践
有了IoC的一个认识之后,我们还需要解释一个概念,这个可以帮助你在后面的应用以及源码阅读的时候不至于一脸蒙圈。
1.1、container
容器本身就是一个类实例,而inversify要做的就是利用这么一个类实例来管理诸多别的类实例,而且依靠一套有序的方法实现,这点我们在待会的文章中会深入解释。
容器本身还有父容器和子容器的概念,所以Container对象有一个字段parent来表示,这样可以做到继承。这个概念在使用Container.resolve
的时候有用到。
1.2、scope
在inversify.js中,或者说是在IoC的概念中存在一个叫做scope的单词,它是和class的注入关联在一起的。一个类的注入scope可以支持以下三种模式:
- Transient:每次从容器中获取的时候(也就是每次请求)都是一个新的实例
- Singleton:每次从容器中获取的时候(也就是每次请求)都是同一个实例
- Request:社区里也成为Scoped模式,每次请求的时候都会获取新的实例,如果在这次请求中该类被require多次,那么依然还是用同一个实例返回
前面两种模式好理解,一个是瞬时的,一个是单例,所谓瞬时就是类似一个http请求,请求结束的时候也就销毁掉了。比较难理解的是Request。Request其实是一个特殊的Singleton,大家都知道单例模式就是整个生命周期(使用unbind可以结束这个生命周期)只会实例化一次,后续获取的都是实例化后的缓存,而Request模式也是利用缓存,但是它不是整个绑定的生命周期,而是在每次获取实例的时候(使用的方法包括:container.get、container.getTagged 和 container.getNamed)。怎么理解呢?每当调用上面讲的这些方法的时候,inversify.js都会解析根依赖和对应的子依赖,好比如这样:
@injectable()
class A {
constructor(
@inject('Author') author: Author
@inject('Summary') summary: Summary
)
}
@injectable()
class Author {
constructor(
@inject('Description') description: Description
)
}
上面的class A依赖了Author和Summary两个实例,当执行这么一次依赖注入的时候会去分析Author这个类,分析的时候发现它依赖了Description,这样就形成了一份依赖树,其中那些被重复依赖的类实例将会使用缓存的实例,而不是每次都全新创建,这种方式可以在解析阶段减少很多的工作,在某些情况下可以用来做性能优化。
Scope可以全局配置,通过defaultScope
参数传参进去,也可以针对每个类进行区别配置,使用方法是:
.inSingletonScope()
、.inTransientScope()
、inRequestScope
1.3、typescript中的修饰器
Ts的修饰器的功能目前部分有别于原生ES的修饰器实现,本文主要提提ts修饰器支持的参数修饰和属性修饰。
所谓参数修饰就是在类构造器或类方法的形参内使用修饰器,而属性修饰是在类属性上直接应用修饰器,比如下面的例子(仅仅为示意才这么使用):
class Greeter {
@defaultValue('greeting')
greeting: string;
@validate
hello(@isNumber number: number) {
return `${this.greeting || getDefault(this, 'greeting')} ${number}`;
}
}
那么根据官网的要求,我们实现上面的两个修饰器就应该有如下的函数原型:
import "reflect-metadata";
const defaultMetadataKey = Symbol("default");
const isNumberMetadataKey = Symbol("isNumber");
function defaultValue(defaultString: string) {
return Reflect.metadata(defaultMetadataKey, defaultString);
}
function getDefault(target: any, propertyKey: string) {
return Reflect.getMetadata(defaultMetadataKey, target, propertyKey);
}
function isNumber(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const allIsNumberParameters: number[] = Reflect.getOwnMetadata(isNumberMetadataKey, target, propertyKey) || [] // 这里的propertyKey就是constructor
allIsNumberParameters.push(parameterIndex)
Reflect.defineMetadata(isNumberMetadataKey, allIsNumberParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value;
descriptor.value = function () {
let allIsNumberParameters: number[] = Reflect.getOwnMetadata(isNumberMetadataKey, target, propertyName);
if (allIsNumberParameters) {
for (let parameterIndex of allIsNumberParameters) {
if (parameterIndex >= arguments.length || typeof arguments[parameterIndex] !== 'number') {
throw new Error("argument is not number.");
}
}
}
return method.apply(this, arguments);
}
}
2、inversify的实现思路
根据官网的wiki:(InversifyJS/architecture.md at master · inversify/InversifyJS · GitHub介绍,我们得以可以大致了解到整个IoC的实现思路,本段大部分翻译自该文档,稍加一些个人的理解。
Inversify的实现思路很大程度上受到了 C # 版本的IoC:[GitHub - ninject/Ninject: the ninja of .net dependency injectors]的影响,但是因为 C# 与javascript的不同,所以内部代码设计与C#版本并不一样,不过用到的一些术语和解析阶段是基本一致。
2.1、实现架构
Inversify在真正解析一个依赖之前会执行三个必须的操作(另外包含两个可选操作):
- 注解阶段(Annotation)
- 计划阶段(Planning)
- 中间件(这个是可选的步骤)
- 解析阶段(Resolution)
- 激活(这个也是可选的步骤)
Inversify的代码目录根据这些流程来命名:
2.1.1、注解阶段
注解阶段将会去读取修饰器产生的元数据并将其传输到一系列的Request
和Target
类实例中。接着Request
和Target
实例将在Planing
阶段中被用来生成一份解析计划
(resolution plan)
2.1.2、计划阶段
当我们执行下面语句的时候:
var obj = container.get<SomeType>("SomeType");
Inversify会开始一段新的解析,意思就是容器会创建一份新的解析上下文。解析的上下文包含了对容器的索引和Plan
实例的索引。
Plan
是Plan
类的实例,Plan
包含了对上下文的索引以及根请求的索引,其中所谓的请求表示的是注入到Target
的一个依赖。也就是解析一个依赖就会创建一次Request
。
接下来我们看一下下面的代码片段:
@injectable()
class FooBar implements FooBarInterface {
public foo : FooInterface;
public bar : BarInterface;
public log() {
console.log("foobar");
}
constructor(
@inject("FooInterface") foo : FooInterface,
@inject("BarInterface") bar : BarInterface
) {
this.foo = foo;
this.bar = bar;
}
}
var foobar = container.get<FooBarInterface>("FooBarInterface");
上面的代码片段会生成一个新的Context
和一个新的Plan
。该Plan
包含了一个带着null Target
的RootRequest
和两个子Request
:
- 第一个子请求代表着
FooInterface
这个依赖,并且它的target是一个构造器参数,名为“foo”
- 第二个子请求代表着
BarInterface
这个依赖,并且它的target是一个构造器参数,名为”bar”
官网给的一张示意图帮你理解整个解析的过程:
2.1.3、中间件阶段
如果我们配置了一些中间件,那么它将会在解析阶段之前被执行掉。中间件可以用来开发一些浏览器扩展,这样就可以允许我们使用一些图形化工具比如d3.js来展示解析计划。这类工具帮助开发者在开发阶段更好地诊断问题的所在。
2.1.4、解析阶段
把Plan
实例传给Resolver
实例,然后Resolver
实例会从依赖树中从叶子节点开始逐一处理依赖直到根节点。这个处理过程可以以同步或者异步的方式执行,异步的话可以提高性能~
2.1.5、激活阶段
当一个依赖被解析之后,并且在它被加入到缓存(如果是单例的话)并注入之前(也就是返回结果之前)会发生激活行为。这样就允许开发者添加的事件处理器在激活阶段完成之前被调用。这个特性允许开发者做一些别的事情,比如注入一个代理以拦截注入对象的方法或属性的调用。
3、inversify的绑定过程
上一小节说的是依赖是如何从容器中取出来的,那么这一小节我们说说依赖是如何注入进去的,以及inversify提供了多少种类型绑定方法。
初学inversify的人很容易被他的绑定语法搞蒙圈,到处都是bind().toXX.whenXXX().
。其实主要是你不清楚这些方法的含义,导致你很难接受这种一目了然的写法。
那么我们首先来说说这些写法的意义。
下图是inversify根据绑定后的操作不同分门别类的所有文件:
从文件命名来看,我们知道inversify提供了诸如to
、when
、on
这类的方法供我们决定一个类型绑定的诸多属性。
接下来记住这句话: 除了to语法,其余的语法其实都是在往Binding这个类实例的属性赋值
就是下面的这些属性:
所有的入口都是指向BindingToSyntax这个类,再往外衍生出各种when
语法。
BindingToSyntax
:指定class绑定到容器内的类型,从写法来说:container.bind(A).toXXX()
,很好理解成绑定类A为(to)XXX。这里的XXX可以有以下几种:- to():必须传入一个构造器,定义的类型是:
BindingTypeEnum.Instance
,后续在使用的时候会new
掉这个构造器 - toSelf():to写法的一种简写方式,内部最后还是调用to
- toConstantValue():绑定为常量,传入的是一个初始化后的实例,定义的类型是:
BindingTypeEnum.ConstantValue
- toDynamicValue():绑定为动态值,在获取的时候会去执行对应的函数,定义的类型是:
BindingTypeEnum.DynamicValue
- toConstructor():绑定为构造函数,在获取之后需要自己实例化,定义的类型是:
BindingTypeEnum.Constructor
- toFactory():绑定为工厂函数,与刚才的动态值不一样,动态值会执行完动态函数返回值,而工厂函数则会返回一个高阶函数,允许你进一步定制值,定义的类型是:
BindingTypeEnum.Factory
- toFunction():绑定为函数,其实就是toConstantValue的别名,定义的类型为:
BindingTypeEnum.Function
- toAutoFactory():绑定为自动工厂函数,此时的工厂函数不用开发者提供,内部自己实现掉了,定义的类型为:
BindingTypeEnum.Factory
- toProvider():绑定为一个异步的工厂函数,称之为Provider,对于需要一些异步操作的时候这种方式非常有用,定义的类型为:
toProvider
- toService():绑定为一个服务,让其解析为以前声明过的别的类型绑定,这个绑定很特殊,没有别的任何后续操作,因为它没有返回值,看代码就知道:
- to():必须传入一个构造器,定义的类型是:
public toService(service: string | symbol | interfaces.Newable<T> | interfaces.Abstract<T>): void {
this.toDynamicValue(
(context) => context.container.get<T>(service)
);
}
- 上面的所有绑定除了最后一个,都会返回一个
when/on/in
语法供开发者往绑定里面加入更多的元素,比如一些限制条件、指定生效scope等等,接下来的演变如下图,只有to
和toDynamicValue
才支持in
操作,所有其走的路线是inWhenOn
,其余的都是WhenOn
路线:
从上图可以看出所有的调用终结于onActivation。上图的箭头给出了对应的调用关系。
而上面所讲的Binding
类中赋值得到的类型和别的参数,在解析依赖的时候就会这么被用到:
将这张图片结合上面的各种to
语法,你就能理解各种写法的含义了。
4、Inversify-binding-decorators和inversify-inject-decorators介绍
为了更好更丝滑地使用inversify,社区提供了下面两个工具库,来简化很多写法。
4.1、inversify-inject-decorators
该工具库主要提供了lazyInject
之类的方法,除了字面上所说的惰性,另外一个非常重要的功能就是允许你将inversifyJs集成到任何自己控制类实例创建的库或者框架,比如react。什么意思呢?
比如下面这个例子:
import { inject, injectable, named, Container } from 'inversify'
import 'reflect-metadata'
interface Weapon {}
@injectable()
class Katana implements Weapon {}
@injectable()
class Shuriken implements Weapon {}
interface Ninja {
katana: Weapon;
shuriken: Weapon;
}
class Ninja implements Ninja {
@inject("Weapon") @named("strong")
public katana: Weapon;
@inject("Weapon") @named("weak")
public shuriken: Weapon;
public constructor() {}
}
const container = new Container()
container.bind<Weapon>("Weapon").to(Katana).whenTargetNamed("strong");
container.bind<Weapon>("Weapon").to(Shuriken).whenTargetNamed("weak");
const ninja: Ninja = new Ninja()
console.log(ninja.katana)
如果Ninja这个类不受我们控制,自己初始化了,但是又需要用到我们容器里存在的Weapon
绑定,那么这个时候上面的写法是获取不到任何绑定的,只会返回undefined。
这个时候就可以考虑使用inversify-inject-decorators
这个库,改写Ninja的实现:
class Ninja implements Ninja {
@lazyInjectNamed("Weapon", "strong")
public katana: Weapon;
@lazyInjectNamed("Weapon", "weak")
public shuriken: Weapon;
public constructor() {}
}
这样就可以获取到Weapon
这个绑定了。实现的原理其实很简单,在调用getDecorators
的时候把container传值进去,之后改写装饰的属性或者参数的setter和getter函数,利用container.get获取值并缓存起来,这样就完成了整个包的实现。至于第一个例子中获取不到值是因为`injectable和inject其实都只是添加元数据,从而辅助容器在注入绑定和解析绑定的时候提供一定的信息,本质上的取值并不靠这个修饰器
它提供的几种写法在Inversify上有其对应的: @inject => @lazyInject @inject @named => @lazyInjectNamed @inject @tagged => @lazyInjectTagged @multiInject => @lazyMultiInject
4.2、 inversify-binding-decorators
如果说上面的那个工具库是为了hack某些东西,那么这个库纯粹就是为了写更少的代码。直接引用官网的图片:
如果用原生inversify的话,需要写右边一大堆代码,如果引用这个库,那么一个修饰器就可以搞定了。最后再统一调用container.load(buildProviderModule());
原理其实也很简单,就是利用@provide
收集所有的需要绑定的注入,然后统一传给container.load
这个可以接受数组的方法。
除了provide
,还提供了fluentProvider
,后面这个可以支持原先在上一小节说的那些when/on/in的语法。比如下面这样的:
@fluentProvide(TYPE.Weapon).whenTargetTagged("throwable", true).done();
class Katana implements Weapon {
public hit() {
return "cut!";
}
}
我们平时使用的时候,更喜欢将这个抽象成一个函数,比如:
const provideThrowable = function(identifier, isThrowable) {
return provide(identifier)
.whenTargetTagged("throwable", isThrowable)
.done();
};
@provideThrowable(TYPE.Weapon, true)
class Katana implements Weapon {
public hit() {
return "cut!";
}
}
参考:
- GitHub - tc39/proposal-decorators: Decorators for ES6 classes
- Dependency injection in React using InversifyJS
- Decorators · TypeScript
- InversifyJS/architecture.md at master · inversify/InversifyJS · GitHub