原生js读取properties_带你学习inversify.js系列 - inversify基础知识学习

阅读本文前,请熟练掌握以下最最基本概念: 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在真正解析一个依赖之前会执行三个必须的操作(另外包含两个可选操作):

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

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

cbaa47f6c24ecfc2425fd5e26a45cf33.png

2.1.1、注解阶段

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

2.1.2、计划阶段

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

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

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

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

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

86ecef7732f411498dd5b153fa75635a.png

2.1.3、中间件阶段

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

2.1.4、解析阶段

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

2.1.5、激活阶段

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

3、inversify的绑定过程

上一小节说的是依赖是如何从容器中取出来的,那么这一小节我们说说依赖是如何注入进去的,以及inversify提供了多少种类型绑定方法。

初学inversify的人很容易被他的绑定语法搞蒙圈,到处都是bind().toXX.whenXXX().。其实主要是你不清楚这些方法的含义,导致你很难接受这种一目了然的写法。

那么我们首先来说说这些写法的意义。

下图是inversify根据绑定后的操作不同分门别类的所有文件:

51cc7d21ee427154a58823c84a730df4.png

从文件命名来看,我们知道inversify提供了诸如towhenon这类的方法供我们决定一个类型绑定的诸多属性。

接下来记住这句话: 除了to语法,其余的语法其实都是在往Binding这个类实例的属性赋值

就是下面的这些属性:

98865daa1d2edd6ec48f41763bc4d8d4.png

所有的入口都是指向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():绑定为一个服务,让其解析为以前声明过的别的类型绑定,这个绑定很特殊,没有别的任何后续操作,因为它没有返回值,看代码就知道:
public toService(service: string | symbol | interfaces.Newable<T> | interfaces.Abstract<T>): void {
        this.toDynamicValue(
            (context) => context.container.get<T>(service)
        );
    }
  • 上面的所有绑定除了最后一个,都会返回一个when/on/in语法供开发者往绑定里面加入更多的元素,比如一些限制条件、指定生效scope等等,接下来的演变如下图,只有totoDynamicValue才支持in操作,所有其走的路线是inWhenOn,其余的都是WhenOn路线:

630d722a6cee8f328ace204cc2cfd423.png

从上图可以看出所有的调用终结于onActivation。上图的箭头给出了对应的调用关系。

而上面所讲的Binding类中赋值得到的类型和别的参数,在解析依赖的时候就会这么被用到:

1c2bb213a023a3c9c4b6046cbdcd2912.png

将这张图片结合上面的各种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某些东西,那么这个库纯粹就是为了写更少的代码。直接引用官网的图片:

b0601a1ffd0c1c5fe84c78fa53b30642.png

如果用原生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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值