InversifyJS介绍
前文讲到,Theia使用InversifyJS实现依赖注入。本文介绍一下InversifyJS。
InversifyJS 官网 https://inversify.io/,github地址:https://github.com/inversify/InversifyJS,是一个轻量级的,typescript编写的,支持JavaScript & Node.js的依赖注入工具。
具体怎么用,看了下面的例子就明白了。
整体项目的文件列表如下:
所有文件我打了下包,放在这里:https://files.cnblogs.com/files/blogs/759971/inversify.zip,有需要可以下载研究,代码我粘贴如下:
interfaces.ts
/** * 这个文件定义了3个名词,以及这3个名词可以干什么动词。 * 一般接口的定义模式就是名词里面套动词 */ // 这是个战士 export interface Warrior { fight(): string; //这个战士可以战斗 sneak(): string; //他也可以偷袭 } // 这是个武器 export interface Weapon { hit(): string; //可以打击 } //这是个能扔出去的武器,比如类似标枪、石头之类的东西 export interface ThrowableWeapon { throw(): string; //扔 }
// file entities.ts import { injectable, inject } from "inversify"; import "reflect-metadata"; import { Weapon, ThrowableWeapon, Warrior } from "./interfaces"; import { TYPES } from "./types"; // 日本武士刀,实现了Weapon这个接口。 // 接口一般是一个抽象名词,类会比接口具像一些。比如可以把 人类 定义成一个接口,然后 学生、老师 可以定义成实现了人类这个接口的类,学生和老师相比人类,会有更具像、明显的特征。 // @injectable 表示这个类可以被绑定到container里 @injectable() class Katana implements Weapon { public hit() { return "cut!"; } } // 手里剑,实现了ThrowableWeapon这个接口。 // 手里剑是日本对脱手暗器的统称,脱手暗器就是出手后不再收回再次使用的暗器。手里剑分为棒状手里剑和车剑。棒状手里剑有单尖的,俗称飞针,双尖的,名为千本。 // 车剑就是俗话说的忍者镖,中国叫流星镖,有3-8个尖,盘式飞行。棒状手里剑携带轻便,制作简单,但使用起来难度高,需要长久的练习达到一定的技术才可以使用,技术的高低直接影响着射程的远近和准度。 对于棒状手里剑的国内练习者在网络上成立了一个暗器联盟,主要是推广棒状手里剑的使用技术。 @injectable() class Shuriken implements ThrowableWeapon { public throw() { return "hit!"; } } // 这个忍者实现了战士的接口 @injectable() class Ninja implements Warrior { private _katana: Weapon; private _shuriken: ThrowableWeapon; private calledCount : number = 0; // 忍者 初始化需要两个参数,类型是 Weapon 和 ThrowableWeapon // 这里inject的用途表示这两个参数,不是用户创建Ninja实例的时候传的参数。而是在myContainer.bind的时候从myContainer自己内部map上按TYPES的key取到的 public constructor( @inject(TYPES.WeaponKey) katana: Weapon, @inject(TYPES.ThrowableWeaponKey) shuriken: ThrowableWeapon ) { this._katana = katana; this._shuriken = shuriken; this.calledCount = this.calledCount+1; //加这个是为了演示一下每次myContainer.get都是一个新实例,和单例模式不一样 console.log("Ninja inited, " + this.calledCount); } public fight() { return this._katana.hit(); } public sneak() { return this._shuriken.throw(); } } // 这个钢铁侠实现了战士的接口 @injectable() class IronMan implements Warrior { private _katana: Weapon; private _shuriken: ThrowableWeapon; // 忍者 初始化需要两个参数,类型是 Weapon 和 ThrowableWeapon public constructor( @inject(TYPES.WeaponKey) katana: Weapon, @inject(TYPES.ThrowableWeaponKey) shuriken: ThrowableWeapon ) { this._katana = katana; this._shuriken = shuriken; console.log("IronMan inited"); // cut! } public fight() { return this._katana.hit(); } public sneak() { return "I am Esquire, I wouldn't do such a thing! !"; } } export { Ninja, Katana, Shuriken, IronMan};
// file types.ts // 关于Symbol的用法, 讲的不错的一个文章:https://zhuanlan.zhihu.com/p/297923315 // 这里就是为了给一个标识,相当于定义了四个map的key一样 const TYPES = { NinjaKey: Symbol.for("Ninja"), IronManKey: Symbol.for("IronMan"), WeaponKey: Symbol.for("Weapon"), ThrowableWeaponKey: Symbol.for("ThrowableWeapon") }; export { TYPES };
inversify.config.ts
// file inversify.config.ts import { Container } from "inversify"; import { TYPES } from "./types"; import { Warrior, Weapon, ThrowableWeapon } from "./interfaces"; import { Ninja, IronMan, Katana, Shuriken } from "./entities"; // 这里相当于新建了一个map const myContainer = new Container(); //这里相当于要创建一个Ninja类实例,并放在myContainer这个map里,用的key是TYPES里取的常量 myContainer.bind<Warrior>(TYPES.NinjaKey).to(Ninja); //忍者 myContainer.bind<Warrior>(TYPES.IronManKey).to(IronMan); //钢铁侠 myContainer.bind<Weapon>(TYPES.WeaponKey).to(Katana); myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeaponKey).to(Shuriken); export { myContainer };
main.ts,执行入口
import { myContainer } from "./inversify.config"; import { TYPES } from "./types"; import { Warrior } from "./interfaces"; // 经过我仔细琢磨,myContainer可以理解为一个map. // myContainer.get表示从map里取值,key来自TYPES定义 const ninja = myContainer.get<Warrior>(TYPES.NinjaKey); // 每次get都会新建一个实例,ninja1和上面的ninja是两个独立的实例,不是单例模式 // const ninja1 = myContainer.get<Warrior>(TYPES.NinjaKey); const ironMan = myContainer.get<Warrior>(TYPES.IronManKey); console.log(ninja.fight()); // cut! console.log(ninja.sneak()); // hit! console.log(ironMan.fight()); // cut! console.log(ironMan.sneak()); // I am Esquire, I wouldn't do such a thing! !
tsconfig.json
{ "compilerOptions": { "target": "es5", "lib": ["dom","es6"], "types": ["reflect-metadata"], "module": "commonjs", "moduleResolution": "node", "experimentalDecorators": true, "emitDecoratorMetadata": true } }
README.md
# ts文件运行方式 1. 安装typescript: npm install -g typescript 2. 编译ts: tsc xxx.ts,会生成xxx.js文件 3. 运行: node xxx.js # 运行本示例 2. 在本项目目录下执行: tsc --outDir build 3. 运行:node build/main.js # 编译注意 如果使用 “tsc xxx.ts” 命令进行编译,那么是不会读取当前目录的配置文件tsconfig.json的,而是直接使用内置的配置,这时候的 target 默认是 es5,所以识别不了 es6 的 symbol; 而如果是使用 “tsc” 命令编译,不带文件名,则可以读取到配置文件;如果需要指定读取的配置文件的路径,可以使用 -p 路径参数。可以执行tsc -h 查看tsc指令的完整使用说明。
执行上面的示例工程,需要终端进入到项目根目录,执行:
tsc --outDir build
node build/main.js
运行结果:
Ninja inited, 1 IronMan inited cut! hit! cut! I am Esquire, I wouldn't do such a thing! !
总结
如果我们不用这种依赖注入的方式。常规方式就是:
1、创建两个武器实例
2、再创建Nijia的实例,传入上面两个参数。如下:
const katana = new Katana() const shuriken = new Shuriken() const ninja = new Ninja(katana, shuriken)
ninja.fight()
这二者的区别是什么呢?
如果不用DI依赖注入的方式,而用常规的这种方式,当我们调用第三方写的类的时候,需要知道非常多的细节,比如上面,调用Ninja还需要知道Katana和Shuriken类,而且如果有一些Ninja换武器了,不用Katana了,第三方修改了他的代码,我们也得跟着改。
如果用DI的方式,我们就不需要关注Katana和Shuriken类。第三方实现的是myContainer.bind<Warrior>(TYPES.NinjaKey).to(Ninja); 把他的实现放在container里就行了。我们用的时候只需要像main.ts里那样。get出来,然后调用就好了。根本不关注他fight的时候用是的什么武器,第三方有一天换武器了,我们也不需要改代码。
依赖注入怎么理解呢?就是Ninja依赖的类,Katana和Shuriken,是框架自动注入进去的。有的资料里也叫依赖反转,指依赖关系本来要我们调用方处理,变成包的提供方处理了。