在 React 前端框架里使用 InversifyJS

By | 7月 27, 2026

InversifyJS 是一个强大且轻量级的 IoC (Inversion of Control) container,适用于由 TypeScript 驱动的 JavaScript 和 Node.js 应用程序。

Github: https://github.com/inversify/inversifyjs

什么是控制反转、依赖注入

没有 IoC 的例子

class Engine {
}

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }
}

Car 自己创建 Engine,控制着 Engine 的创建过程。耦合度高,要换新的 Engine,需要修改 Car 代码。

解决这种耦合关系,就是把对象创建和管理权交出去。这个思想就是 IoC 控制反转

class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }
}

Engine engine = new Engine();
Car car = new Car(engine);

把被依赖的对象塞给依赖者的过程,就叫做 依赖注入

  • IoC 是理念
  • DI 是技术手段

依赖注入 (Dependency Injection) 是实现 IoC 的常见手段,常见的方式有三种:

构造器 注入 – 最推荐

class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }
}

Setter 注入 – 可选项

当 构造器 注入有循环依赖的问题时,可用 Setter 注入。

class Car {
    private Engine engine;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}

字段 注入 – 不推荐

Spring 中常见,虽然写起来简单,但测试不方便,因此不太推荐。

InversifyJS 从 Version 7 之后,就去掉了字段注入(如要使用就退回 inversify@^6)。

@Component
public class Car {

    @Autowired
    private Engine engine;
}

React 项目配置 Inversify

安装依赖

Dependencies: pnpm add inversify reflect-metadata tslib

DEV Dependencies: pnpm add -D babel-plugin-transform-typescript-metadata

tsconfig.ts 中启用 Experimental decorators 和 Emit Decorator Metadata

{
  "extends": "./src/.umi/tsconfig.json",
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  ......
}

React 项目通常使用 Babel 来 transpile 代码,上面声明的 emitDecoratorMetadata 在运行时不生效,需要额外配置一个 plugin。对于 UmiJS 项目,修改 .umirc.ts,添加:

import { defineConfig } from "@umijs/max";

export default defineConfig({
  ......
  extraBabelPlugins: [
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { legacy: true }],
  ],
});

在 React 最顶端 (UmiJs 是 app.tsx),引入 reflect-metadata:

import 'reflect-metadata'; // Required for inversify

从 Spring 开发者角度类比 InversifyJS

容器里的对象,Sring 叫 Bean,InversifyJS 叫 Service。

InversifyJSSpring
@injectable()@Component / @Service标记 class 可以被容器管理
@inject()@Autowired / @Inject给 field 注入实例
container.bind()Component Scan把 class 实例加入容器
ContainerApplicationContext容器
inSingletonScope()@Scope("singleton")单例模式
toDynamicValue()@Bean Factory Method动态创建 bean

InversifyJS 基本使用

@injectable(‘Transient’ | ‘Singleton’ | ‘Request’)

推荐给每一个类都加 @injectable。

  • Transigent: 默认值,每次 get 都返回一个新的实例。
  • Singleton: 容器只创建一次实例。
  • Request: 同一次对象解析树共享实例。例如: 类的构造方法两个参数类型都是 UserService (Reqeust scole),container 会给这两个参数 inject 相同的实例。

@inject()

给参数注入实例。Inversify 目前只支持 constructor 注入,property 注入从 version 7 之后就去掉了。

p.s.

如果类有 @injectable() 注解,constructor 里的参数即使没有 @inject,容器也会自动注入。推荐始终写上 @inject,代码明确。

把 bean 添加到 container

调用 container 的 bind 方法。InversifyJS 没有 autoscan,所有 bean 都要手动添加到 container。

bind<SERVICE_TYPE>(ServiceIdentifier).to(SERVICE)

  • ServiceIdentifier: string | symbol | class | abstract-class

对于接口类型,通常用 symbol 表示。

从 container 获取 bean

调用 container 的 get 方法。

get<T>(ServiceIdentifier)

简单完整实例

import { injectable, inject, Container } from 'inversify';

// 1. Create a new container
export const SERVICE_CONTAINER = new Container();


// 2. Define an interface and a class that implements it
export interface IUserService {
  getUserName(): string;
}

@injectable()
export class UserService implements IUserService {
  getUserName(): string {
    return "Alex";
  }
}


// 3. Bind the interface to the class in the container
export const TYPES = {
  IUserService: Symbol.for("IUserService")
};
SERVICE_CONTAINER.bind<IUserService>(TYPES.IUserService).to(UserService);


// 4. Create a controller that depends on the service
@injectable()
export class UserController {

  constructor(
    @inject(TYPES.IUserService)
    private userService: IUserService
  ) { }

  print() {
    console.log(this.userService.getUserName());
  }
}

SERVICE_CONTAINER.bind(UserController).toSelf();


// 5. Now you can retrieve the controller from the container and use it
SERVICE_CONTAINER.get(UserController).print();

使用指南

named – 同一个 identifier 绑定多个 bean

可以给 bean 起名字,类似 Spring @Qualifier(“bean_name”)。

const container: Container = new Container();
container.bind<Weapon>('Weapon').to(Sword).whenNamed('sword');
container.bind<Weapon>('Weapon').to(Gun).whenNamed('gun');

@injectable()
class Warrior {
  public sword: Weapon;
  public gun: Weapon;
  constructor(
    @inject('Weapon') @named('sword') sword: Weapon,
    @inject('Weapon') @named('gun') gun: Weapon,
  ) {
    this.sword= sword;
    this.gun= gun;
  }
}

container.bind(Warrior).toSelf();

const warrior: Warrior = container.get(Warrior);

@taged – 同一个 identifier 绑定多个 bean

和 named 类似,name 是一个 参数,tag 是两个参数 (tagName, tagValue)。

@injectable()
class Ninja {
  public katana: Weapon;
  public shuriken: Weapon;
  constructor(
    @inject('Weapon') @tagged('weaponKind', 'melee') katana: Weapon,
    @inject('Weapon') @tagged('weaponKind', 'ranged') shuriken: Weapon,
  ) {
    this.katana = katana;
    this.shuriken = shuriken;
  }
}

const container: Container = new Container();
container.bind<Weapon>('Weapon').to(Katana).whenTagged('weaponKind', 'melee');
container
  .bind<Weapon>('Weapon')
  .to(Shuriken)
  .whenTagged('weaponKind', 'ranged');
container.bind(Ninja).toSelf();

const ninja: Ninja = container.get(Ninja);

Container parent 层级

const childContainer: Container = new Container({ parent: parentContainer });

通过 parent 属性,多个 container 就可以形成一个 container 链条。

从这种 chained container 里 get service,有两种情况。

标准解析:get 一个 service

在标准解析模式(默认行为)下,InversifyJS 遵循首次发现的方法:

  1. 首先,它在当前容器中搜索绑定
  2. 如果在当前容器中找到绑定,则仅使用这些绑定
  3. 如果在当前容器中未找到绑定,则向上移动到父容器
  4. 此过程一直持续到找到绑定或到达顶级容器为止

这意味着,如果子容器对某个服务有任何绑定,则父容器对同一服务的绑定将被完全忽略:

链式解析:get 多个 services

链式解析模式允许你从容器层级结构的所有级别收集绑定。当使用带有 chained: true 选项的 getAll() 或 getAllAsync() 时:

  1. 从当前容器收集绑定
  2. 然后从父容器收集绑定
  3. 这将递归地向上继续整个层级结构
  4. 所有收集到的绑定将被合并并返回

当你想要聚合来自应用程序不同层的服务(例如,来自父容器的核心服务和来自子容器的特定于功能的服务)时,这特别有用。

const weapons: Weapon[] = container.getAll<Weapon>(‘Weapon’, { chained: true });

toDynamicValue

将函数绑定到给定的服务标识符。每当解析服务时,都会调用传递的函数来构建解析值。

const container: Container = new Container();
container.bind<Weapon>('Weapon').toDynamicValue((): Weapon => new Katana());

const katana: Weapon = container.get<Weapon>('Weapon');

inXXXScope() – 绑定时指定定 scope

container.bind(Cat).toSelf().inTransientScope();
container.bind(Dog).toSelf().inSingletonScope();
container.bind(Bird).toSelf().inRequestScope();

@postConstruct

类被创建后,会执行 @postConstruct 标注的方法。

interface Weapon {
  damage: number;
}

export class Katana implements Weapon {
  #damage: number = 10;

  public get damage(): number {
    return this.#damage;
  }

  @postConstruct()
  public improve(): void {
    this.#damage += 2;
  }
}

const container: Container = new Container();
container.bind<Weapon>('Weapon').to(Katana);

// Katana.damage is 12
const katana: Weapon = container.get<Weapon>('Weapon');

@preDestroy

类被销毁前,会执行 @preDestroy 标注的方法。

interface Weapon {
  damage: number;
}

class Katana implements Weapon {
  readonly #damage: number = 10;

  public get damage(): number {
    return this.#damage;
  }

  @preDestroy()
  public onDeactivation(): void {
    console.log(`Deactivating weapon with damage ${this.damage.toString()}`);
  }
}

const container: Container = new Container();

container.bind<Weapon>('Weapon').to(Katana).inSingletonScope();

container.get('Weapon');

container.unbind('Weapon');

@optional

表示这个属性是可选的,container 如果没有该实例,则不注入。

@injectable()
class Ninja {
  public katana: Weapon;
  public shuriken: Weapon | undefined;
  constructor(
    @inject('Katana') katana: Weapon,
    @inject('Shuriken') @optional() shuriken: Weapon | undefined,
  ) {
    this.katana = katana;
    this.shuriken = shuriken;
  }
}

@multiInject – 注入 all matched

@injectable()
class Ninja {
  constructor(
     @multiInject(weaponServiceId)
    private weapons: Weapon[]
  ) { }
}