InversifyJS 基础示例:从传统依赖到依赖注入
这篇用一个经典的“忍者-武器”例子,展示如何从“手动 new 依赖”演进到“用 InversifyJS 做依赖注入”。
这个例子虽然简单,但涵盖了 InversifyJS 最核心的概念:@injectable()、@inject()、Container、绑定关系。
下面是一个可运行的完整示例,你可以在 CodeSandbox 里直接编辑和运行:
代码解析:一步步理解依赖注入
第一步:定义服务标识符(Symbol)
1const TYPES = {
2 Ninja: Symbol('Ninja'),
3 Katana: Symbol('Katana'),
4 Shuriken: Symbol('Shuriken')
5};
这里定义了三个 Symbol 作为服务标识符。在 InversifyJS 里,Symbol 是推荐的服务标识方式,因为:
- 避免字符串冲突(两个不同的 Symbol 永远不会相等)。
- 在 TypeScript 里可以配合泛型做类型安全(
container.bind<Interface>(TYPES.Service).to(Implementation))。
在 Theia 里,你经常会看到类似的模式:
1export const FileService = Symbol('FileService');
2export const CommandRegistry = Symbol('CommandRegistry');
第二步:定义业务类
1class Katana {
2 hit() {
3 return 'cut!';
4 }
5}
6
7class Shuriken {
8 throw() {
9 return 'hit!';
10 }
11}
12
13class Ninja {
14 private _katana: Katana;
15 private _shuriken: Shuriken;
16
17 constructor(katana: Katana, shuriken: Shuriken) {
18 this._katana = katana;
19 this._shuriken = shuriken;
20 }
21
22 fight() {
23 return this._katana.hit();
24 }
25
26 sneak() {
27 return this._shuriken.throw();
28 }
29}
这是三个普通的类:
Katana(武士刀)和Shuriken(手里剑)是“武器”类,各自有攻击方法。Ninja(忍者)依赖这两个武器,通过构造函数注入。
传统写法的问题:如果不用依赖注入,你得这样创建 Ninja:
1const katana = new Katana();
2const shuriken = new Shuriken();
3const ninja = new Ninja(katana, shuriken);
这样写的问题:
- 每次创建
Ninja都要手动 new 依赖,代码重复。 - 如果
Katana或Shuriken的构造函数变了,所有创建Ninja的地方都要改。 - 测试时很难 mock 依赖(比如想测试
Ninja但不想真的创建Katana)。
第三步:标记类为可注入
1import * as inversify from 'inversify';
2import 'reflect-metadata';
3
4// 方式一:用 decorate 函数(适合不能直接用装饰器语法的场景)
5inversify.decorate(inversify.injectable(), Katana);
6inversify.decorate(inversify.injectable(), Shuriken);
7inversify.decorate(inversify.injectable(), Ninja);
8inversify.decorate(inversify.inject(TYPES.Katana), Ninja, 0);
9inversify.decorate(inversify.inject(TYPES.Shuriken), Ninja, 1);
这里用 decorate 函数手动给类添加装饰器:
inversify.decorate(inversify.injectable(), Class):标记类为“可注入的”。inversify.decorate(inversify.inject(Symbol), Class, index):标记构造函数的第index个参数需要注入哪个服务。
更常见的写法(装饰器语法):
1@injectable()
2class Katana {
3 hit() {
4 return 'cut!';
5 }
6}
7
8@injectable()
9class Shuriken {
10 throw() {
11 return 'hit!';
12 }
13}
14
15@injectable()
16class Ninja {
17 constructor(
18 @inject(TYPES.Katana) private _katana: Katana,
19 @inject(TYPES.Shuriken) private _shuriken: Shuriken
20 ) {}
21
22 fight() {
23 return this._katana.hit();
24 }
25
26 sneak() {
27 return this._shuriken.throw();
28 }
29}
这种写法更简洁,也是 Theia 里最常见的模式。
第四步:注册绑定关系
1const container = new inversify.Container();
2
3container.bind(TYPES.Ninja).to(Ninja);
4container.bind(TYPES.Katana).to(Katana);
5container.bind(TYPES.Shuriken).to(Shuriken);
这里创建了一个 Container(容器),然后把所有服务注册进去:
container.bind(Symbol).to(Class):告诉容器“当需要Symbol标识的服务时,创建Class的实例”。
第五步:从容器获取实例
1const ninja = container.get<Ninja>(TYPES.Ninja);
2console.log(ninja.fight()); // 输出: "cut!"
3console.log(ninja.sneak()); // 输出: "hit!"
现在不需要手动 new 了,直接 container.get() 就能拿到 Ninja 实例,容器会自动:
- 发现
Ninja需要Katana和Shuriken。 - 先创建
Katana和Shuriken的实例。 - 把这两个实例传给
Ninja的构造函数。 - 返回创建好的
Ninja实例。
这个例子说明了什么?
从传统写法到依赖注入,最大的变化是:
- 之前:
Ninja的创建者需要知道它依赖什么,手动组装。 - 现在:
Ninja的创建者只需要说“给我一个Ninja”,容器负责组装所有依赖。
这个模式在 Theia 里非常常见:
1// Theia 扩展里,你只需要这样写:
2@injectable()
3export class MyContribution implements CommandContribution {
4 constructor(
5 @inject(CommandRegistry) private commands: CommandRegistry,
6 @inject(FileService) private fileService: FileService
7 ) {}
8
9 // ... 使用 commands 和 fileService
10}
Theia 的容器会在启动时自动创建 MyContribution 实例,并注入它需要的 CommandRegistry 和 FileService,你不需要关心这些服务是怎么创建的、在哪里创建的。
下一步:更复杂的场景
这个基础例子展示了最简单的依赖注入,但实际项目里还会遇到:
- 接口绑定:绑定到接口而不是具体类(
bind<Interface>(Symbol).to(Implementation))。 - 作用域:单例(
inSingletonScope())vs 每次新建(inTransientScope())。 - 命名绑定和标签:同一个接口有多个实现时如何区分。
- 可选注入:某些依赖可能不存在(
@optional())。
这些会在后续文章里展开。