带你学习inversify.js系列 - inversify基础知识学习(一)
阅读本文前,请熟练掌握以下最最基本概念:
- Reflect
- 修饰器
- 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都会解析根依赖和对应的子依赖,好比如这样:
1@injectable() 2class A { 3 constructor( 4 @inject('Author') author: Author 5 @inject('Summary') summary: Summary 6 ) 7} 8 9@injectable() 10class Author { 11 constructor( 12 @inject('Description') description: Description 13 ) 14}
上面的class A依赖了Author和Summary两个实例,当执行这么一次依赖注入的时候会去分析Author这个类,分析的时候发现它依赖了Description,这样就形成了一份依赖树,其中那些被重复依赖的类实例将会使用缓存的实例,而不是每次都全新创建,这种方式可以在解析阶段减少很多的工作,在某些情况下可以用来做性能优化。
Scope可以全局配置,通过defaultScope
参数传参进去,也可以针对每个类进行区别配置,使用方法是:
.inSingletonScope()
、.inTransientScope()
、inRequestScope
1.3、typescript中的修饰器
Ts的修饰器的功能目前部分有别于原生ES的修饰器实现,本文主要提提ts修饰器支持的参数修饰和属性修饰。
所谓参数修饰就是在类构造器或类方法的形参内使用修饰器,而属性修饰是在类属性上直接应用修饰器,比如下面的例子(仅仅为示意才这么使用):
1class Greeter { 2 @defaultValue('greeting') 3 greeting: string; 4 5 @validate 6 hello(@isNumber number: number) { 7 return `${this.greeting || getDefault(this, 'greeting')} ${number}`; 8 } 9}
那么根据官网的要求,我们实现上面的两个修饰器就应该有如下的函数原型:
1import "reflect-metadata"; 2 3const defaultMetadataKey = Symbol("default"); 4const isNumberMetadataKey = Symbol("isNumber"); 5 6function defaultValue(defaultString: string) { 7 return Reflect.metadata(defaultMetadataKey, defaultString); 8} 9function getDefault(target: any, propertyKey: string) { 10 return Reflect.getMetadata(defaultMetadataKey, target, propertyKey); 11} 12 13function isNumber(target: Object, propertyKey: string | symbol, parameterIndex: number) { 14 const allIsNumberParameters: number[] = Reflect.getOwnMetadata(isNumberMetadataKey, target, propertyKey) || [] // 这里的propertyKey就是constructor 15 allIsNumberParameters.push(parameterIndex) 16 Reflect.defineMetadata(isNumberMetadataKey, allIsNumberParameters, target, propertyKey); 17} 18function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) { 19 let method = descriptor.value; 20 descriptor.value = function () { 21 let allIsNumberParameters: number[] = Reflect.getOwnMetadata(isNumberMetadataKey, target, propertyName); 22 if (allIsNumberParameters) { 23 for (let parameterIndex of allIsNumberParameters) { 24 if (parameterIndex >= arguments.length || typeof arguments[parameterIndex] !== 'number') { 25 throw new Error("argument is not number."); 26 } 27 } 28 } 29 30 return method.apply(this, arguments); 31 } 32}
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、计划阶段
当我们执行下面语句的时候:
1var obj = container.get<SomeType>("SomeType");
Inversify会开始一段新的解析,意思就是容器会创建一份新的解析上下文。解析的上下文包含了对容器的索引和Plan
实例的索引。
Plan
是Plan
类的实例,Plan
包含了对上下文的索引以及根请求的索引,其中所谓的请求表示的是注入到Target
的一个依赖。也就是解析一个依赖就会创建一次Request
。
接下来我们看一下下面的代码片段:
1@injectable() 2class FooBar implements FooBarInterface { 3 public foo : FooInterface; 4 public bar : BarInterface; 5 public log() { 6 console.log("foobar"); 7 } 8 constructor( 9 @inject("FooInterface") foo : FooInterface, 10 @inject("BarInterface") bar : BarInterface 11 ) { 12 this.foo = foo; 13 this.bar = bar; 14 } 15} 16 17var 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、激活阶段
当一个依赖被解析之后,并且在它被加入到缓存(如果是单例的话)并注入之前(也就是返回结果之前)会发生激活行为。这样就允许开发者添加的事件处理器在激活阶段完成之前被调用。这个特性允许开发者做一些别的事情,比如注入一个代理以拦截注入对象的方法或属性的调用。
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 带你学习inversify.js系列 - inversify基础知识学习(一) 的内容有疑问,请在下面的评论系统中留言,谢谢。