InversifyJS 是面向 TypeScript 和 JavaScript 的依赖注入容器。它将“创建对象”和“使用对象”分开:业务类声明自己依赖什么,容器负责按绑定规则创建并组装对象。
本文使用 InversifyJS 传统的装饰器 API。不同大版本的安装和初始化细节可能不同,但容器、绑定、作用域和条件匹配的概念相同。
最小示例
先定义一个服务标识。接口只存在于 TypeScript 编译期,运行时不能作为容器的 key,因此通常使用 Symbol 或字符串:
import "reflect-metadata";
import { Container, inject, injectable } from "inversify";
const TYPES = {
Logger: Symbol.for("Logger"),
};
interface Logger {
info(message: string): void;
}
@injectable()
class ConsoleLogger implements Logger {
public info(message: string) {
console.log(message);
}
}
@injectable()
class UserService {
public constructor(@inject(TYPES.Logger) private readonly logger: Logger) {}
public createUser(name: string) {
this.logger.info(`create user: ${name}`);
}
}
const container = new Container();
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger);
container.bind<UserService>(UserService).toSelf();
container.get(UserService).createUser("Alice");这个例子包含三个角色:
@injectable()让类可以被容器构造;@inject(TYPES.Logger)声明构造函数参数需要哪个服务;bind(...).to(...)注册服务标识和具体实现的对应关系。
调用 container.get(UserService) 时,容器先找到 UserService 的绑定,读取构造函数参数的注入信息,再按 TYPES.Logger 找到 ConsoleLogger 的绑定,最后完成实例化。
Container 的职责
Container 保存绑定规则,并从某个服务标识开始解析完整依赖图。应用通常只在启动阶段创建和配置容器;业务代码通过构造函数接收依赖,不需要在每个类中保存或传递 Container。
const container = new Container();
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger);
container.bind<UserService>(UserService).toSelf();
const service = container.get(UserService);常见的绑定方式如下:
// 绑定到可由容器构造的类
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger);
// 类本身作为服务标识
container.bind<UserService>(UserService).toSelf();
// 绑定已有对象
container.bind<Logger>(TYPES.Logger).toConstantValue(consoleLogger);
// 每次解析时运行工厂函数
container.bind<Date>(TYPES.Now).toDynamicValue(() => new Date());toConstantValue 适合配置、已经初始化的 SDK 客户端等明确共享的对象。toDynamicValue 适合依赖运行时上下文的值;工厂函数的执行次数仍受后续作用域设置影响。
容器解析失败通常由以下原因引起:服务没有绑定、被注入的类没有 @injectable()、运行环境没有在入口加载 reflect-metadata,或者同一个标识有多个绑定但调用了 get。后者应改用 getAll,或补充条件绑定以消除歧义。
三种作用域
作用域决定容器在何时复用实例。InversifyJS 的常用作用域是 Transient、Singleton 和 Request。
Transient:每个注入点创建新实例
Transient 是类绑定的默认作用域。每次容器需要这个服务时都会创建一个新对象,即使这些请求属于同一次 container.get 调用。
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger).inTransientScope();例如 UserService 和 AuditService 都依赖 Logger,并且它们在一次对象图解析中都需要该服务,Transient 绑定会创建两个 ConsoleLogger 实例。没有状态、创建成本低的服务适合这一作用域。
Singleton:同一容器复用一个实例
Singleton 会在容器第一次解析该绑定时创建实例,之后该容器中对相同绑定的解析都会返回这个实例。
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger).inSingletonScope();单例的边界是 Container,不是整个进程。两个不同容器各自会有自己的 ConsoleLogger。如果单例对象持有可变请求状态,后续请求可能读到前一次请求的数据;这类状态应放到请求上下文或显式参数中。
Request:一次解析树内复用实例
Request 的“请求”指一次根解析操作,例如一次 container.get(UserService),并不等同于 HTTP 请求。容器在解析这棵依赖树时,会复用同一绑定第一次创建的实例;下一次 container.get 会重新创建。
container.bind<Logger>(TYPES.Logger).to(ConsoleLogger).inRequestScope();下面的例子中,left 与 right 是同一个根对象解析过程中得到的两个 TraceContext,所以它们指向同一个实例:
@injectable()
class TraceContext {}
@injectable()
class UsesTrace {
public constructor(@inject(TYPES.Trace) public readonly trace: TraceContext) {}
}
@injectable()
class Handler {
public constructor(
@inject(UsesTrace) public readonly left: UsesTrace,
@inject(UsesTrace) public readonly right: UsesTrace,
) {}
}
container.bind<TraceContext>(TYPES.Trace).to(TraceContext).inRequestScope();
container.bind<UsesTrace>(UsesTrace).toSelf();
container.bind<Handler>(Handler).toSelf();
const first = container.get(Handler);
const second = container.get(Handler);
first.left.trace === first.right.trace; // true
first.left.trace === second.left.trace; // falseRequest 作用域适合在一次解析内共享的临时对象。它不能自动跟踪异步调用链,也不能自动映射到 Web 框架的一次 HTTP 请求;需要按 HTTP 请求共享数据时,应在应用层建立明确的请求容器或上下文管理方案。
用名称和标签选择实现
同一个服务标识可以绑定多个实现。此时容器需要额外条件来判断在某个注入点应该使用哪一个实现。
const TYPES = {
Weapon: Symbol.for("Weapon"),
};
interface Weapon {
hit(): string;
}
@injectable()
class Katana implements Weapon {
public hit() {
return "cut";
}
}
@injectable()
class Shuriken implements Weapon {
public hit() {
return "throw";
}
}@named 与 whenTargetNamed
名称条件是一个字符串。注入点通过 @named 声明名称,绑定通过 whenTargetNamed 匹配它:
import { inject, named } from "inversify";
container.bind<Weapon>(TYPES.Weapon).to(Katana).whenTargetNamed("melee");
container.bind<Weapon>(TYPES.Weapon).to(Shuriken).whenTargetNamed("ranged");
@injectable()
class Ninja {
public constructor(
@inject(TYPES.Weapon) @named("melee") public readonly melee: Weapon,
@inject(TYPES.Weapon) @named("ranged") public readonly ranged: Weapon,
) {}
}文档中的 targetName 指的是依赖目标的参数名或属性名;它可以参与匹配,但不能像 @named 一样在注入点显式传入业务值。参数名在压缩、重构后可能变化,优先用 @named 表示稳定的业务分类。
@tagged 与 whenTargetTagged
标签由键和值组成,适合一个服务需要多个筛选维度的场景。例如按环境选择配置:
import { inject, tagged } from "inversify";
const TAGS = {
Environment: "environment",
};
interface ApiEndpoint {
readonly url: string;
}
container
.bind<ApiEndpoint>(TYPES.ApiEndpoint)
.toConstantValue({ url: "https://api.example.com" })
.whenTargetTagged(TAGS.Environment, "production");
container
.bind<ApiEndpoint>(TYPES.ApiEndpoint)
.toConstantValue({ url: "https://api-staging.example.com" })
.whenTargetTagged(TAGS.Environment, "staging");
@injectable()
class ApiClient {
public constructor(
@inject(TYPES.ApiEndpoint)
@tagged(TAGS.Environment, "production")
public readonly endpoint: ApiEndpoint,
) {}
}名称可以看作固定键下的单个字符串值;标签提供键和值两个维度。只有“近战、远程”这类单一分类时,@named 更直接;需要同时表达环境、区域、协议等维度时,使用 @tagged 更合适。
使用时的边界
依赖注入解决的是对象创建与依赖装配问题,不会自动改善模块设计。服务标识过多、条件绑定层层叠加时,依赖关系会变得难以追踪。更稳妥的做法是让绑定模块按业务边界组织,为大多数依赖保留一个默认实现,只在确实需要替换实现的位置使用名称或标签。
测试时可以为测试容器注册替代实现,或覆盖特定绑定,让业务类继续通过构造函数接收依赖。这样测试无需修改业务类,也能控制外部服务、时间和随机数等不稳定输入。

