InversifyJS 基础示例:从传统依赖到依赖注入

这篇用一个经典的“忍者-武器”例子,展示如何从“手动 new 依赖”演进到“用 InversifyJS 做依赖注入”。
这个例子虽然简单,但涵盖了 InversifyJS 最核心的概念:@injectable()@inject()Container、绑定关系。

下面是一个可运行的完整示例,你可以在 CodeSandbox 里直接编辑和运行:

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 依赖,代码重复。
  • 如果 KatanaShuriken 的构造函数变了,所有创建 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 实例,容器会自动:

  1. 发现 Ninja 需要 KatanaShuriken
  2. 先创建 KatanaShuriken 的实例。
  3. 把这两个实例传给 Ninja 的构造函数。
  4. 返回创建好的 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 实例,并注入它需要的 CommandRegistryFileService,你不需要关心这些服务是怎么创建的、在哪里创建的。

这个基础例子展示了最简单的依赖注入,但实际项目里还会遇到:

  • 接口绑定:绑定到接口而不是具体类(bind<Interface>(Symbol).to(Implementation))。
  • 作用域:单例(inSingletonScope())vs 每次新建(inTransientScope())。
  • 命名绑定和标签:同一个接口有多个实现时如何区分。
  • 可选注入:某些依赖可能不存在(@optional())。

这些会在后续文章里展开。