@evyweb/ioctopus

中文文档
1.4.1发布于 2026/05/09MITTypeScriptESM + CJS零依赖近 30 天下载 5.4万

JavaScript 和 TypeScript 用于类和函数的简单 IoC 容器.

中文使用文档
文档目录11 节
  1. 导言
  2. 安装
  3. 如何使用
  4. 设立书记官处
  5. 创建容器
  6. 注册依赖关系
  7. 使用登记册解决依赖关系(建议)
  8. 在没有登记册的情况下解决依赖性
  9. 模块
  10. 使用范围
  11. 循环依赖关系

TypeScript 的简单 IOC 容器

NPM Version GitHub Actions Workflow Status codecov

NPM Downloads NPM Downloads

logo-ioctopus.png

导言

用于TypeScript的IoC(倒置控制)容器.

它背后的想法是创建一个简单的容器,可以注册和解决类和函数的依赖性而无需依赖反射元数据.

它使用平版Script代码,因此可以添加到任何项目上而无需额外的依赖.

它也在"Next.js"中软件和"节点"和"边缘跑道"中工作.

安装

npm i @evyweb/ioctopus

如何使用

要使用章鱼,需要创建容器并绑起自属物. 为此,您需要为您想要注册的每个受抚养人创建ID.

这个ID,我们称之为"注入符",可以是字符串或符号. (请注意,你必须一致:要么总是使用字符串来约束和解决依赖关系,要么总是使用符号;你不能混合它们. )

然后可以将依赖绑定在一个值,一个函数,一个类,一个工厂,一个更高级的函数,或者一个曲折的函数上.

说明: 采用安全型登记册是改进类型安全和开发者经验的建议办法.

您可以定义一个注册类型或接口,以获取您依赖性注射密钥及其相应类型的全部类型安全性. 这就不需要在解决依赖性时进行人工铸造.

容器还验证您提供的依赖性与您的类或函数所期望的匹配。 TypeScript 将在编译时捕获错误,例如省略所需的依赖性或提供错误的依赖类型.

设立书记官处

定义将您的注射符映射到想要的类型的类型或界面 :

// Using strings as injection tokens
type MyRegistry = {
    'USER_SERVICE': UserService;
    //...
}

// Or using symbols
const USER_SERVICE = Symbol('USER_SERVICE');
type MyRegistry = {
  [USER_SERVICE]: UserService;
  //...
}

创建容器

用您的注册类型创建您的容器 :

import { createContainer } from '@evyweb/ioctopus';

// Create a type-safe container (Recommended)
const container = createContainer<MyRegistry>();
// Old way, without type safety
const container = createContainer();

注册依赖关系

初级

你可以注册原始人.

// using strings
container.bind('DEP1').toValue('dependency1');
container.bind('DEP2').toValue(42);

// using symbols
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);

职能

  • 无需依赖即可注册函数 :
const sayHelloWorld = () => console.log('Hello World');

// using strings
container.bind('SIMPLE_FUNCTION').toFunction(sayHelloWorld);

// or using symbols
container.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld);

咖喱鱼

  • 您可以使用 Currying (1级 Currying) 注册依赖函数 :
const myFunction = (dep1: string, dep2: number) => (name: string) => console.log(`${dep1} ${dep2} ${name}`);

// using strings
container.bind('CURRIED_FUNCTION_WITH_DEPENDENCIES')
    .toCurry(myFunction, ['DEP1', 'DEP2']);

// or using symbols
container.bind(DI.CURRIED_FUNCTION_WITH_DEPENDENCIES)
    .toCurry(myFunction, [DI.DEP1, DI.DEP2]);
  • 也可以使用依赖对象:
interface Dependencies {
    dep1: string,
    dep2: number
}

const myFunction = (dependencies: Dependencies) => (name: string) => console.log(`${dependencies.dep1} ${dependencies.dep2} ${name}`);

// The dependencies will be listed in an object in the second parameter
// using strings
container.bind('CURRIED_FUNCTION_WITH_DEPENDENCIES')
    .toCurry(myFunction, {dep1: 'DEP1', dep2: 'DEP2'});

// or using symbols
container.bind(DI.CURRIED_FUNCTION_WITH_DEPENDENCIES)
    .toCurry(myFunction, {dep1: DI.DEP1, dep2: DI.DEP2});

上级职能

您也可以通过使用更高排序函数来注册依赖函数 :

const MyServiceWithDependencies = (dep1: string, dep2: number): MyServiceWithDependenciesInterface => {
    return {
        runTask: () => {
            // Do something with dep1 and dep2
        }
    };
};

// The dependencies will be listed in an array in the second parameter
// using strings
container.bind('HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES')
    .toHigherOrderFunction(MyServiceWithDependencies, ['DEP1', 'DEP2']);

// or using symbols
container.bind(DI.HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES)
    .toHigherOrderFunction(MyServiceWithDependencies, [DI.DEP1, DI.DEP2]);

但如果你喜欢,也可以使用依赖对象:

interface Dependencies {
    dep1: string,
    dep2: number
}

const MyService = (dependencies: Dependencies): MyServiceInterface => {
    return {
        runTask: () => {
            // Do something with dependencies.dep1 and dependencies.dep2
        }
    };
};

// The dependencies will be listed in an object in the second parameter
// using strings
container.bind('HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES')
    .toHigherOrderFunction(MyService, {dep1: 'DEP1', dep2: 'DEP2'});

// or using symbols
container.bind(DI.HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES)
    .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2});

工厂

对于更复杂的案件,您可以注册工厂.

// using strings
container.bind('MY_USE_CASE').toFactory(() => {
    // Do something before creating the instance
     
    // Then return the instance
    return MyUseCase({
        myService: container.get<MyService>('MY_SERVICE')
    });
});

// or using symbols
container.bind(DI.MY_USE_CASE).toFactory(() => {
    // Do something before creating the instance
     
    // Then return the instance
    return MyUseCase({
        myService: container.get<MyService>(DI.MY_SERVICE)
    });
});

类

您可以注册类; 类的依赖性将被解决并注入构建器

class MyServiceClass implements MyServiceClassInterface {
    constructor(
        private readonly dep1: string,
        private readonly dep2: number,
    ) {}

    runTask(): string {
        return `Executing with dep1: ${this.dep1} and dep2: ${this.dep2}`;
    }
}

container.bind('CLASS_WITH_DEPENDENCIES').toClass(MyServiceClass, ['DEP1', 'DEP2']);

// or using symbols
container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);

但如果你喜欢,也可以使用依赖对象:


interface Dependencies {
    dep1: string,
    dep2: number
}

class MyServiceClass implements MyServiceClassInterface {
    constructor(private readonly dependencies: Dependencies) {}

    runTask(): string {
        return `Executing with dep1: ${this.dependencies.dep1} and dep2: ${this.dependencies.dep2}`;
    }
}

container.bind('CLASS_WITH_DEPENDENCIES').toClass(MyServiceClass, {dep1: 'DEP1', dep2: 'DEP2'});

// or using symbols
container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, {dep1: DI.DEP1, dep2: DI.DEP2});
  • 无需依赖即可注册课程:
class MyServiceClassWithoutDependencies implements MyServiceClassInterface {
    runTask(): string {
        return `Executing without dependencies`;
    }
}

container.bind('CLASS_WITHOUT_DEPENDENCIES').toClass(MyServiceClassWithoutDependencies);

// or using symbols
container.bind(DI.CLASS_WITHOUT_DEPENDENCIES).toClass(MyServiceClassWithoutDependencies);

您现在可以用容器的获取方法解决依赖性 .

import { DI } from './di';

// Primitive
// using strings
const dep1 = container.get('DEP1'); // type will be the one you registered in your registry (string here)

// or using symbols
const dep1 = container.get(DI.DEP1); // type will be the one you registered in your registry (string here)

在没有登记册的情况下解决依赖性

import { DI } from './di';

// Primitive
// using strings
const dep1 = container.get<string>('DEP1'); // By default, the type will be 'unknown', so you need to cast it manually

// or using symbols
const dep1 = container.get<string>(DI.DEP1); // By default, the type will be 'unknown', so you need to cast it manually

模块

您也可以使用模块来组织您的依赖。 模块支持与集装箱相同的类型安全登记办法.

正在装入模块

然后可将模块装入容器. 默认情况下,在创建容器时,会使用罩下的一个默认模块.

有书记官处(建议):

interface AppRegistry {
    'DEP1': string;
    'DEP2': number;
    'MY_SERVICE': MyServiceInterface;
}

const module1 = createModule<AppRegistry>();
module1.bind('DEP1').toValue('dependency1');

const module2 = createModule<AppRegistry>();
module2.bind('DEP2').toValue(42);

const module3 = createModule<AppRegistry>();
module3.bind('MY_SERVICE').toHigherOrderFunction(MyService, {dep1: 'DEP1', dep2: 'DEP2'});

const container = createContainer<AppRegistry>();
container.load('module1', module1);
container.load('module2', module2);
container.load('module3', module3);

const myService = container.get('MY_SERVICE'); // Type: MyServiceInterface

未登记旧做法:

const module1 = createModule();
module1.bind(DI.DEP1).toValue('dependency1');

const module2 = createModule();
module2.bind(DI.DEP2).toValue(42);

const module3 = createModule();
module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2});

const container = createContainer();
container.load(Symbol('module1'), module1);
container.load(Symbol('module2'), module2);
container.load(Symbol('module3'), module3);

const myService = container.get<MyServiceInterface>(DI.MY_SERVICE);

依赖性不需要在使用它们的模块中注册. 注意作为密钥使用的模块名可以是符号或字符串.

模块覆盖

您也可以覆盖模块的依赖性 。 模块的依赖性将被上一个已上载模块的依赖性所压倒.

const module1 = createModule<AppRegistry>();
module1.bind(DI.DEP1).toValue('OLD dependency1');
module1.bind(DI.MY_SERVICE).toFunction(sayHelloWorld);

const module2 = createModule<AppRegistry>();
module2.bind(DI.DEP1).toValue('NEW dependency1');

const module3 = createModule<AppRegistry>();
module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2});

const container = createContainer<AppRegistry>();
container.bind(DI.DEP2).toValue(42); // Default module
container.load(Symbol('module1'), module1);
container.load(Symbol('module2'), module2);
container.load(Symbol('module3'), module3);

// The dependency DI.MY_SERVICE will be resolved with the higher-order function and dep1 will be 'NEW dependency1'
const myService = container.get(DI.MY_SERVICE);

卸载模块

也可以从容器中卸下一个模块. 模块的依赖性将从容器中去除. 已缓存实例将被删除,以保持一致性并避免可能出错.

const module1 = createModule<AppRegistry>();
module1.bind(DI.DEP1).toValue('dependency1');

const container = createContainer<AppRegistry>();
container.load(Symbol('module1'), module1);

container.unload(Symbol('module1'));

// Will throw an error as the dependency is not registered anymore
const myService = container.get(DI.DEP1); 

使用范围

单通范围( 默认)

在单顿范围内,容器每次解决依赖性时都会返回同样的情况.

container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);
// or
container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'singleton');

const instance1 = container.get(DI.MY_SERVICE);
const instance2 = container.get(DI.MY_SERVICE);

console.log(instance1 === instance2); // true

临时范围

在瞬态范围,容器每次解决依赖性时都会返回一个新的实例.

container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'transient');

const instance1 = container.get(DI.MY_SERVICE);
const instance2 = container.get(DI.MY_SERVICE);

console.log(instance1 === instance2); // false

范围

在范围范围内,容器在一个范围内返回同一情况。 不同的范围会有不同的情况.

要使用范围范围,您需要使用 runInScope 创建范围.

container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped');
const instance1 = undefined;
const instance2 = undefined;

container.runInScope(() => {
    instance1 = container.get(DI.MY_SERVICE);
    instance2 = container.get(DI.MY_SERVICE);

    console.log(instance1 === instance2); // true
});

container.runInScope(() => {
    const instance3 = container.get(DI.MY_SERVICE);

    console.log(instance3 === instance1); // false
});

注:如果试图解决范围外的依赖性,则会出现出错.

循环依赖关系

章鱼容器可以探测到循环依赖性. 如果检测到循环依赖, 则会出现出错 .

const container = createContainer<AppRegistry>();

const A_TOKEN = Symbol('A');
const B_TOKEN = Symbol('B');

class A {
    constructor(public b: B) {}
}

class B {
    constructor(public a: A) {}
}

container.bind(A_TOKEN).toClass(A, [B_TOKEN]);
container.bind(B_TOKEN).toClass(B, [A_TOKEN]);

container.get(A_TOKEN); // Will throw: "Circular dependency detected: Symbol(A) -> Symbol(B) -> Symbol(A)"

这样可以避免无限循环和堆叠溢出的错误.

版本与发布记录

@evyweb/ioctopus 共发布 12 个版本,首次发布于 2024/09/08;过去 12 个月发布了 3 个版本。

版本发布日期距今
1.4.1latest2026/05/096 个月前
1.4.02026/03/237 个月前
1.3.12025/11/1511 个月前
1.3.02025/09/211.1 年前
1.2.02024/12/071.9 年前
1.1.02024/11/272 年前

关于 @evyweb/ioctopus 的常见问题

@evyweb/ioctopus 是什么?

JavaScript 和 TypeScript 用于类和函数的简单 IoC 容器.

如何安装 @evyweb/ioctopus?

在项目目录运行 npm install @evyweb/ioctopus,或使用 pnpm add / yarn add / bun add。

@evyweb/ioctopus 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@evyweb/ioctopus 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@evyweb/ioctopus 可以商用吗?

@evyweb/ioctopus 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@evyweb/ioctopus 的下载量有多少?

根据 npm 官方下载统计,@evyweb/ioctopus 在 2026/10/14 – 2026/11/12 这 30 天内被下载 54,476 次,日均约 1,816 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@evyweb/ioctopus 还在维护吗?

最新版本 1.4.1 发布于 2026/05/09(6 个月前);至今共发布 12 个版本,其中过去 12 个月发布了 3 个;首次发布于 2024/09/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。