65.9K
CodeProject 正在变化。 阅读更多。
Home

InversifyJS 2.0 简介:一款强大的轻量级 IoC 容器,用于支持 TypeScript 的 JavaScript 应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016年3月14日

CPOL

5分钟阅读

viewsIcon

25169

一个强大的 IoC 容器,适用于由 TypeScript 提供支持的 JavaScript 应用程序

引言

InversifyJS 是一个开源的控制反转(IoC)容器,用于 TypeScript 应用程序。

关于

InversifyJS 是一个强大的轻量级(4KB)的微型控制反转(IoC)容器,适用于 TypeScript 和 JavaScript 应用。微型 IoC 容器使用类构造函数来识别和注入其依赖项。InversifyJS 拥有友好的 API,并鼓励使用最佳的 OOP 和 IoC 实践。

动机

JavaScript 应用程序的规模日益增长。InversifyJS 的设计旨在使 JavaScript 开发人员能够编写符合 SOLID 原则的代码。

理念

InversifyJS 的开发有 3 个主要目标

  1. 允许 JavaScript 开发人员编写符合 SOLID 原则的代码。

  2. 促进和鼓励遵守最佳的 OOP 和 IoC 实践。

  3. 尽量减少运行时开销。

证言

Nate Kohari - Ninject 的作者

“我是 Ninject 的作者。做得很好!我曾尝试了几次为 JavaScript 和 TypeScript 创建 DI 框架,但缺少 RTTI 确实阻碍了事情的进展。 ES7 元数据让我们走到了这一步(正如你所发现的)。继续加油!”

背景

我大约在一年前发布了 InversifyJS 1.0。当时它有一些不错的特性,比如瞬态和单例作用域,但离我想要达到的目标还很远。

InversifyJS 2.0 (v2.0.0-alpha.3) 终于来了,我非常激动能够向你展示它的强大功能。

InversifyJS 2.0 是一个轻量级库,因为我的主要目标之一是尽量减少运行时开销。最新的构建仅为 4 KB(gzip),并且没有依赖项。

另一个不错的优点是 InversifyJS 可以在 Web 浏览器和 Node.js 中使用。但是,在某些环境中,某些功能可能需要一些 ES6 polyfills(例如,Providers 需要 Promises,Proxy 需要 ES6 Proxies)。

注意:此版本是预发布版本,尚未准备好投入生产使用。其中一些 API 并非最终版本,可能会发生更改。

让我们来看看 InversifyJS 的基本用法和 API。

基础(TypeScript)

让我们来看看 InversifyJS 在 TypeScript 中的基本用法和 API。

步骤 1:声明接口

我们的目标是编写符合依赖倒置原则的代码。这意味着我们应该“依赖于抽象,而不是依赖于具体实现”。让我们从声明一些接口(抽象)开始。

interface INinja {
    fight(): string;
    sneak(): string;
}

interface IKatana {
    hit(): string;
}

interface IShuriken {
    throw();
}

步骤 2:实现接口并使用 `@inject` 装饰器声明依赖项

让我们继续声明一些类(具体实现)。这些类是我们刚刚声明的接口的实现。

import { inject } from "inversify";

class Katana implements IKatana {
    public hit() {
        return "cut!";
    }
}

class Shuriken implements IShuriken {
    public throw() {
        return "hit!";
    }
}

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katana: IKatana, shuriken: IShuriken) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}

步骤 3:创建和配置 Kernel

我们建议在一个名为 `inversify.config.ts` 的文件中执行此操作。这是唯一存在一些耦合的地方。在你应用程序的其余部分,你的类应该没有对其他类的引用。

import { Kernel } from "inversify";

import { Ninja } from "./entities/ninja";
import { Katana } from "./entities/katana";
import { Shuriken} from "./entities/shuriken";

var kernel = new Kernel();
kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IKatana>("IKatana").to(Katana);
kernel.bind<IShuriken>("IShuriken").to(Shuriken);

export default kernel;

步骤 4:解析依赖项

你可以使用 `Kernel` 类的 `get()` 方法来解析依赖项。请记住,你应该只在你的组合根中这样做,以避免服务定位器反模式

import kernel = from "./inversify.config";

var ninja = kernel.get<INinja>("INinja");

expect(ninja.fight()).eql("cut!"); // true
expect(ninja.sneak()).eql("hit!"); // true

正如我们所见,`IKatana` 和 `IShuriken` 已成功解析并注入到 `Ninja` 中。

基础(JavaScript)

为了获得最佳开发体验,推荐使用 TypeScript,但如果你愿意,也可以使用纯 JavaScript。以下代码片段在 Node.js v5.71 中实现了之前的示例,但未使用 TypeScript。

var inversify = require("inversify");
require("reflect-metadata");

var TYPES = {
    Ninja: "Ninja",
    Katana: "Katana",
    Shuriken: "Shuriken"
};

class Katana {
    hit() {
        return "cut!";
    }
}

class Shuriken {
    throw() {
        return "hit!";
    }
}

class Ninja {
    constructor(katana, shuriken) {
        this._katana = katana;
        this._shuriken = shuriken;
    }
    fight() { return this._katana.hit(); };
    sneak() { return this._shuriken.throw(); };
}

// Declare injections
inversify.inject(TYPES.Katana, TYPES.Shuriken)(Ninja);

// Declare bindings
var kernel = new inversify.Kernel();
kernel.bind(TYPES.Ninja).to(Ninja);
kernel.bind(TYPES.Katana).to(Katana);
kernel.bind(TYPES.Shuriken).to(Shuriken);

// Resolve dependencies
var ninja = kernel.get(TYPES.Ninja);
return ninja;

特性(v2.0.0 alpha.3)

让我们来看看 InversifyJS 的特性!

声明 Kernel 模块

Kernel 模块可以帮助你在非常大的应用程序中管理绑定的复杂性。

let someModule: IKernelModule = (kernel: IKernel) => {
    kernel.bind<INinja>("INinja").to(Ninja);
    kernel.bind<IKatana>("IKatana").to(Katana);
    kernel.bind<IShuriken>("IShuriken").to(Shuriken);
};

let kernel = new Kernel({ modules: [ someModule ] });

控制依赖项的作用域

InversifyJS 默认使用瞬态作用域,但你也可以使用单例作用域。

kernel.bind<IShuriken>("IShuriken").to(Shuriken).inTransientScope(); // Default
kernel.bind<IShuriken>("IShuriken").to(Shuriken).inSingletonScope();

注入值

将抽象绑定到常量值。

kernel.bind<IKatana>("IKatana").toValue(new Katana());

注入类构造函数

将抽象绑定到类构造函数。

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(Katana: INewable<IKatana>, shuriken: IShuriken) {
        this._katana = new Katana();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
kernel.bind<INewable<IKatana>>("INewable<IKatana>").toConstructor<IKatana>(Katana);

注入工厂

将抽象绑定到用户定义的工厂。

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katanaFactory: IFactory<IKatana>, shuriken: IShuriken) {
        this._katana = katanaFactory();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
kernel.bind<IFactory<IKatana>>("IFactory<IKatana>").toFactory<IKatana>((context) => {
    return () => {
        return context.kernel.get<IKatana>("IKatana");
    };
});

自动工厂

将抽象绑定到自动生成的工厂。

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    private _katana: IKatana;
    private _shuriken: IShuriken;

    public constructor(katanaFactory: IFactory<IKatana>, shuriken: IShuriken) {
        this._katana = katanaFactory();
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}
kernel.bind<IFactory<IKatana>>("IFactory<IKatana>").toAutoFactory<IKatana>();

注入 Provider(异步工厂)

将抽象绑定到 Provider。Provider 是一个异步工厂,这在处理异步 I/O 操作时很有用。

@inject("IKatana", "IShuriken")
class Ninja implements INinja {

    public katana: IKatana;
    public shuriken: IShuriken;
    public katanaProvider: IProvider<IKatana>;

    public constructor(katanaProvider: IProvider<IKatana>, shuriken: IShuriken) {
        this.katanaProvider = katanaProvider;
        this.katana= null;
        this.shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); };
    public sneak() { return this._shuriken.throw(); };

}

var ninja = kernel.get<INinja>("INinja");

ninja.katanaProvider()
     .then((katana) => { ninja.katana = katana; })
     .catch((e) => { console.log(e); });
kernel.bind<IProvider<IKatana>>("IProvider<IKatana>").toProvider<IKatana>((context) => {
    return () => {
        return new Promise<IKatana>((resolve) => {
            let katana = context.kernel.get<IKatana>("IKatana");
            resolve(katana);
        });
    };
});

注入代理

可以在注入依赖项之前创建其代理。这有助于使我们的依赖项与诸如缓存或日志记录等横切关注点的实现无关。

interface IKatana {
    use: () => void;
}

class Katana implements IKatana {
    public use() {
        console.log("Used Katana!");
    }
}

interface INinja {
    katana: IKatana;
}

@inject("IKatana")
class Ninja implements INinja {
    public katana: IKatana;
    public constructor(katana: IKatana) {
        this.katana = katana;
    }
}
kernel.bind<INinja>("INinja").to(Ninja);

kernel.bind<IKatana>("IKatana").to(Katana).proxy((katana) => {
    let handler = {
        apply: function(target, thisArgument, argumentsList) {
            console.log(`Starting: ${new Date().getTime()}`);
            let result = target.apply(thisArgument, argumentsList);
            console.log(`Finished: ${new Date().getTime()}`);
            return result;
        }
    };
    katana.use = new Proxy(katana.use, handler);
    return katana;
});
let ninja = kernelget<INinja>();
ninja.katana.use();
> Starting: 1457895135761
> Used Katana!
> Finished: 1457895135762

多重注入

当两个或多个具体实现已绑定到一个抽象时,我们可以使用多重注入。请注意,通过构造函数将 `IWeapon` 数组注入到 `Ninja` 类中。

interface IWeapon {
    name: string;
}

class Katana implements IWeapon {
    public name = "Katana";
}
class Shuriken implements IWeapon {
    public name = "Shuriken";
}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon[]")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(weapons: IWeapon[]) {
        this.katana = weapons[0];
        this.shuriken = weapons[1];
    }
}

我们将 `Katana` 和 `Shuriken` 绑定到 `IWeapon`。

kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IWeapon>("IWeapon").to(Katana);
kernel.bind<IWeapon>("IWeapon").to(Shuriken);

带标签的绑定

当两个或多个具体实现已绑定到一个抽象时,我们可以使用带标签的绑定来修复 `AMBIGUOUS_MATCH` 错误。请注意,`Ninja` 类的构造函数参数已使用 `@tagged` 装饰器进行了注释。

interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @tagged("canThrow", false) katana: IWeapon,
        @tagged("canThrow", true) shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}

我们将 `Katana` 和 `Shuriken` 绑定到 `IWeapon`,但添加了 `whenTargetTagged` 约束以避免 `AMBIGUOUS_MATCH` 错误。

kernel.bind<INinja>(ninjaId).to(Ninja);
kernel.bind<IWeapon>(weaponId).to(Katana).whenTargetTagged("canThrow", false);
kernel.bind<IWeapon>(weaponId).to(Shuriken).whenTargetTagged("canThrow", true);

创建自己的标签装饰器

创建自己的装饰器非常简单。

let throwable = tagged("canThrow", true);
let notThrowable = tagged("canThrow", false);

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @notThrowable katana: IWeapon,
        @throwable shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}

命名绑定

当两个或多个具体实现已绑定到一个抽象时,我们可以使用命名绑定来修复 `AMBIGUOUS_MATCH` 错误。请注意,`Ninja` 类的构造函数参数已使用 `@named` 装饰器进行了注释。

interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        @named("strong")katana: IWeapon,
        @named("weak") shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}

我们将 `Katana` 和 `Shuriken` 绑定到 `IWeapon`,但添加了 `whenTargetNamed` 约束以避免 `AMBIGUOUS_MATCH` 错误。

kernel.bind<INinja>("INinja").to(Ninja);
kernel.bind<IWeapon>("IWeapon").to(Katana).whenTargetNamed("strong");
kernel.bind<IWeapon>("IWeapon").to(Shuriken).whenTargetNamed("weak");

上下文绑定 & @paramNames

`@paramNames` 装饰器用于从上下文约束访问构造函数参数的名称,即使代码被压缩。`constructor(katana, shuriken) { ...` 在压缩后会变成 `constructor(a, b) { ...`,但由于 `@paramNames`,我们仍然可以引用设计时名称 `katana` 和 `shuriken`。

interface IWeapon {}
class Katana implements IWeapon { }
class Shuriken implements IWeapon {}

interface INinja {
    katana: IWeapon;
    shuriken: IWeapon;
}

@inject("IWeapon", "IWeapon")
@paramNames("katana","shuriken")
class Ninja implements INinja {
    public katana: IWeapon;
    public shuriken: IWeapon;
    public constructor(
        katana: IWeapon,
        shuriken: IWeapon
    ) {
        this.katana = katana;
        this.shuriken = shuriken;
    }
}

我们将 `Katana` 和 `Shuriken` 绑定到 `IWeapon`,但添加了一个自定义的 `when` 约束来避免 `AMBIGUOUS_MATCH` 错误。

kernel.bind<INinja>(ninjaId).to(Ninja);

kernel.bind<IWeapon>("IWeapon").to(Katana).when((request: IRequest) => {
    return request.target.name.equals("katana");
});

kernel.bind<IWeapon>("IWeapon").to(Shuriken).when((request: IRequest) => {
    return request.target.name.equals("shuriken");
});

目标字段实现了 `IQueryableString` 接口,以帮助你创建自定义约束。

interface IQueryableString {
  startsWith(searchString: string): boolean;
  endsWith(searchString: string): boolean;
  contains(searchString: string): boolean;
  equals(compareString: string): boolean;
  value(): string;
}

循环依赖

InversifyJS 能够识别循环依赖,并在检测到循环依赖时抛出异常,以帮助你定位问题。

Error: Circular dependency found between services: IKatana and INinja

实时演示(ES6)

你可以在 tonicdev.com 上在线试用 InversifyJS:tonicdev.com

摘要

我想感谢贡献者。这是一段漫长的旅程,但过去一年与 InversifyJS 一起工作是一件很棒的事情。我对它的未来有很多计划,迫不及待地想看到你用它创造出什么。

请尝试 InversifyJS 并告诉我们你的想法。我们希望根据最终用户的实际需求来定义路线图,而做到这一点唯一的方法就是让你告诉我们你的需求和你喜欢或不喜欢的地方。

记住,你可以访问http://inversify.io/GitHub 仓库以了解更多关于 InversifyJS 的信息。

 

 

 
© . All rights reserved.