带你学习inversify.js系列 - inversify基础知识学习(一)

发表于 2020-03-25
更新于 2024-05-23
分类于 技术专栏
阅读量 345
字数统计 5805

阅读本文前,请熟练掌握以下最最基本概念:

  • 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在真正解析一个依赖之前会执行三个必须的操作(另外包含两个可选操作):

  1. 注解阶段(Annotation)
  2. 计划阶段(Planning)
  3. 中间件(这个是可选的步骤)
  4. 解析阶段(Resolution)
  5. 激活(这个也是可选的步骤)

Inversify的代码目录根据这些流程来命名:

2.1.1、注解阶段

注解阶段将会去读取修饰器产生的元数据并将其传输到一系列的RequestTarget类实例中。接着RequestTarget实例将在Planing阶段中被用来生成一份解析计划(resolution plan)

2.1.2、计划阶段

当我们执行下面语句的时候:

1var obj = container.get<SomeType>("SomeType");

Inversify会开始一段新的解析,意思就是容器会创建一份新的解析上下文。解析的上下文包含了对容器的索引和Plan实例的索引。

PlanPlan类的实例,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 TargetRootRequest和两个子Request

  • 第一个子请求代表着FooInterface这个依赖,并且它的target是一个构造器参数,名为“foo”
  • 第二个子请求代表着BarInterface这个依赖,并且它的target是一个构造器参数,名为”bar” 官网给的一张示意图帮你理解整个解析的过程:

2.1.3、中间件阶段

如果我们配置了一些中间件,那么它将会在解析阶段之前被执行掉。中间件可以用来开发一些浏览器扩展,这样就可以允许我们使用一些图形化工具比如d3.js来展示解析计划。这类工具帮助开发者在开发阶段更好地诊断问题的所在。

2.1.4、解析阶段

Plan实例传给Resolver实例,然后Resolver实例会从依赖树中从叶子节点开始逐一处理依赖直到根节点。这个处理过程可以以同步或者异步的方式执行,异步的话可以提高性能~

2.1.5、激活阶段

当一个依赖被解析之后,并且在它被加入到缓存(如果是单例的话)并注入之前(也就是返回结果之前)会发生激活行为。这样就允许开发者添加的事件处理器在激活阶段完成之前被调用。这个特性允许开发者做一些别的事情,比如注入一个代理以拦截注入对象的方法或属性的调用。

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 带你学习inversify.js系列 - inversify基础知识学习(一) 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github