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。
| InversifyJS | Spring | |
|---|---|---|
@injectable() | @Component / @Service | 标记 class 可以被容器管理 |
@inject() | @Autowired / @Inject | 给 field 注入实例 |
container.bind() | Component Scan | 把 class 实例加入容器 |
Container | ApplicationContext | 容器 |
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 遵循首次发现的方法:
- 首先,它在当前容器中搜索绑定
- 如果在当前容器中找到绑定,则仅使用这些绑定
- 如果在当前容器中未找到绑定,则向上移动到父容器
- 此过程一直持续到找到绑定或到达顶级容器为止
这意味着,如果子容器对某个服务有任何绑定,则父容器对同一服务的绑定将被完全忽略:
链式解析:get 多个 services
链式解析模式允许你从容器层级结构的所有级别收集绑定。当使用带有 chained: true 选项的 getAll() 或 getAllAsync() 时:
- 从当前容器收集绑定
- 然后从父容器收集绑定
- 这将递归地向上继续整个层级结构
- 所有收集到的绑定将被合并并返回
当你想要聚合来自应用程序不同层的服务(例如,来自父容器的核心服务和来自子容器的特定于功能的服务)时,这特别有用。
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[]
) { }
}