应用前端的IoC框架——InversifyJS

1,490 阅读5分钟

通过之前的 “前端也要懂的解耦思想“ 系列,我们已经了解了IoC思想和具体的技术实现。今天介绍一个轻量化的IoC框架——InversifyJS,它提供了非常完备的依赖注入(DI)实现,可以直接用于前端应用的开发。

需要更进一步详细了解的童鞋,可以参考Jeff Tian 翻译的InversifyJS中文文档

那就借助 InversifyJS 来具体实践一下IoC思想吧。根据面向接口的编程思想和依赖倒置原则,我们需要先把需求接口抽象出来。这里我们创建了1个战士类型TSoldier和2个武器类型TGunTKnife

// file interfaces.ts

interface TSoldier {
    useGun(victim: string): void,
    useKnife(victim: string): void,
}

interface TWeapon {
    start(victim: string): void
}

interface TGun extends TWeapon {
    fire(victim: string): void
}

interface TKnife extends TWeapon {
    cut(victim: string): void
}

为了从IoC容器获取我们想要的模块,我们需要规定一些特定的标识符作为钥匙,以便于容器识别你想要哪个模块。这其实也意味着每个模块会有一个唯一的标识符,聪明如你,一定会想到用Symbol类型作为标识符:

// file types.ts

const TYPES = {
    Ak47: Symbol.for("Ak47"),
    Ak48: Symbol.for("Ak48"),
    TuLong: Symbol.for("TuLong"),
    SupermanA: Symbol.for("SupermanA"),
    SupermanB: Symbol.for("SupermanB"),
}

export { TYPES }

以下是模块注册进IoC容器以及注册模块间依赖关系的过程。InversifyJS用了依赖注入(DI)的方式,优雅的实现了注册过程。这里有2个非常重要的装饰器函数injectableinject。前者将模块信息注册到IoC容器里,后者则通过属性注入(也可以通过构造函数注入)建立模块间的依赖关系。

// file entities.ts

import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, ThrowableWeapon, Warrior } from "./interfaces"
import { TYPES } from "./types";


@injectable()
class Ak47 implements TGun {
    public fire(victim: string){
        console.log('Ak47 is firing ' + victim)
    }
    public start(victim: string) {
        this.fire(victim)
    }
}

@injectable()
class Ak48 implements TGun {
    public fire(victim: string){
        console.log('Ak48 is firing ' + victim)
    }
    public start(victim: string) {
        this.fire(victim)
    }
}

@injectable()
class TuLong implements TKnife {
    public cut(victim: string){
        console.log('knife is cutting ' + victim)
    }
    public start(victim: string) {
        this.cut(victim)
    }
}

@injectable()
class SupermanA implements TSoldier {
    @inject(TYPES.Ak47) private _gun: Ak47
    @inject(TYPES.TuLong) private _knife: TuLong
    
    public useGun(victim: string) { 
        this._gun.start(victim)
    }
    public useKnife(victim: string) { 
        this._knife.start(victim)
    }
}

@injectable()
class SupermanB implements TSoldier {
    @inject(TYPES.Ak48) private _gun: Ak48
    @inject(TYPES.TuLong) private _knife: TuLong
    
    public useGun() { 
        this._gun.start()
    }
    public useKnife() { 
        this._knife.start()
    }
}

export { Ak47, TuLong, SupermanA, SupermanB }

这两个装饰器函数的源码非常简单,我们可以来看一下具体实现:

import * as ERRORS_MSGS from "../constants/error_msgs";
import * as METADATA_KEY from "../constants/metadata_keys";


function injectable() {
    return function (target) {
        if (Reflect.hasOwnMetadata(METADATA_KEY.PARAM_TYPES, target)) {
            throw new Error(ERRORS_MSGS.DUPLICATED_INJECTABLE_DECORATOR);
        }
        var types = Reflect.getMetadata(METADATA_KEY.DESIGN_PARAM_TYPES, target) || [];
        
        Reflect.defineMetadata(METADATA_KEY.PARAM_TYPES, types, target);
        return target;
    };
}

function inject(serviceIdentifier) {
    return function (target, targetKey, index) {
        if (serviceIdentifier === undefined) {
            throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name));
        }
        var metadata = new Metadata(METADATA_KEY.INJECT_TAG, serviceIdentifier);
        
        if (typeof index === "number") {
            tagParameter(target, targetKey, index, metadata);
        }
        else {
            tagProperty(target, targetKey, metadata);
        }
    };
}

export { injectable, inject }

这里我们遇到了几个稍显陌生的函数Reflect.hasOwnMetadataReflect.defineMetadataReflect.getMetadata。我们都知道 Reflect 全局对象和 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。而 Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候为类与类属性添加和读取元信息。而之所以用Metadata这种方式保存相关信息,可以理解为是避免了对目标模块的侵入和污染。Reflect Metadata 利用健值对对元信息的保存,本质上也是一种WeakMap的映射,但在数据结构上会有更多的设计。InversifyJS的依赖注入就是通过结合Reflect Metadata和装饰器函数来实现的。

Reflect 对象

Reflect对象的主要设计目的是将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。Reflect也让Object操作都变成函数行为。

Reflect Metadata

Reflect Metadata相关方法并不是标准的API,而是由引入的reflect-metadata库提供的扩展能力。Metadata也被称为“元信息”,通常是指需要隐藏在程序内部的与业务逻辑无关的附加信息。TypeScript 在 1.5+ 的版本已经支持它:

  • 安装:npm i reflect-metadata --save
  • 配置tsconfig.json:emitDecoratorMetadata: true Reflect Metadata 的 API 可以用于类或者类的属性上:
  • 通过Reflect.getMetadata("design:type", target, key),装饰器函数能获取属性类型
  • 通过Reflect.getMetadata("design:paramtypes", target, key)可以获取函数参数类型
  • 通过Reflect.getMetadata("design:returntype", target, key)可以获取返回值类型
  • 通过Reflect.defineMetadata("keyName", "value", target, key)可自定义 metadataKey和值,并在合适的时机获取它的值

injectable装饰器把与target对应的key为design:paramtypes(类构造函数参数的类型)的元信息赋值给了key为inversify:paramtypes的元信息,这些元信息可以供模块实例化时消费。

inject装饰器根据传入的标识符(也就是前文中定义的types),实例化一个元信息对象 Metadata,然后根据形参的类型(当装饰器作为参数装饰器时,第三个参数index是该参数在函数形参中的顺序索引,是数字类型的,否则该装饰器是作为属性装饰器使用的。)来调用不同的处理函数保存元信息。

无论是injectable还是inject,它们的作用都是对于元信息的保存,IOC的实例管理能力依旧依赖容器类Container

inject装饰器告诉IoC容器该高层模块的该属性需要赋予该标识符所对应的低层模块的实例,但IoC容器还不知道某个标识符需要映射哪个模块。于是,我们还需要将标识符与特定模块绑定,这样IoC容器就能通过标识符来映射出相关模块了,以便后续调用时通过标识符来准确获取需要的模块:

// file inversify.config.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { TSoldier, TGun, TKnife } from "./interfaces";
import { Ak47, TuLong, SupermanA, SupermanB } from "./entities";


const container = new Container()

container.bind<TGun>(TYPES.Ak47).to(Ak47)
container.bind<TGun>(TYPES.Ak48).to(Ak48)
container.bind<TKnife>(TYPES.TuLong).to(TuLong)
container.bind<TSoldier>(TYPES.SupermanA).to(SupermanA)
container.bind<TSoldier>(TYPES.SupermanB).to(SupermanB)

export { container }

最后我们运行代码,通过InversifyJS的IoC容器完美解析了模块间的依赖关系,并准确的输出结果:

import { container } from "./inversify.config";
import { TYPES } from "./types";
import { TSoldier } from "./interfaces";


const supermanA = container.get<TSoldier>(TYPES.SupermanA)
const supermanB = container.get<TSoldier>(TYPES.SupermanB)

supermanA.useGun('dog')  // Ak47 is firing dog
supermanB.useGun('dog')  // Ak48 is firing dog
supermanA.useKnife('pig')  // knife is cutting pig
supermanB.useKnife('pig')  // knife is cutting pig

前端小白,文中难免有谬误,请各位大佬轻喷!