@exodus/dependency-injection

中文文档
3.3.1发布于 2026/10/14MITTypeScriptES Module近 30 天下载 1.3万

A simple dependency injection and inversion of container

原始使用文档
文档目录8 节
  1. Usage
  2. Overriding dependencies
  3. Optional dependencies
  4. Injection Style
  5. Known Issues / Limitations
  6. Example (headless)
  7. Rationale
  8. Misc (overachievers only)

@exodus/dependency-injection

This is a basic inversion of control container that resolves a dependency graph and instantiates all nodes. You want to use this if you're stitching together an application from dozens of different modules and your imperatively defined dependency DAG is starting to become unmanagable.

There are lots of sophisticated inversion of control containers out there, e.g. inversify, awilix, and others. They're probably great. Someday we may switch to them, but at this point they're overkill and they raise the cognitive load higher than absolutely necessary. This library implements a simple container in a highly opinionated way with very little flexibility. Here is a sample node in a list of dependencies:

[
  ...,
  {
    id: 'harry',
    dependencies: ['scar', 'dobby'],
    factory: ({ scar, dobby }) => {
      // Harry uses his scar to defeat dobby, or however that book goes
    },
    type: 'wizard', // optional
  }
]

As you can see:

  • Each node has a globally unique string id.
  • It lists its dependencies as string ids.
  • It declares a factory function which receives a single object as an argument, with named options mapping 1:1 to ids listed in dependencies
  • It declares an optional type field. You can use this to query the container for nodes where type is a certain value.

The container, given a list of such nodes, will give you all the instances. That's all there is to it!

Usage

Declare nodes and edges, and let the container wire everything together.

import createContainer from '@exodus/dependency-injection'

const createLogger =
  (namespace) =>
  (...args) =>
    console(namespace, ...args)

const container = createContainer({ logger })

container.registerMultiple([
  {
    id: 'storage',
    factory: createStorage,
  },
  {
    id: 'assetsModule',
    // the container will pass `({ logger })` to the factory
    factory: createAssetsModule,
    dependencies: ['logger'],
  },
  {
    id: 'walletAccountsAtom',
    factory: () =>
      createInMemoryAtom({
        defaultValue: { [WalletAccount.DEFAULT_NAME]: WalletAccount.DEFAULT },
      }),
  },
  {
    id: 'enabledWalletAccountsAtom',
    // the container will pass `({ walletAccountsAtom })` to the factory
    factory: createEnabledWalletAccountsAtom,
    dependencies: ['walletAccountsAtom'],
  },
  {
    id: 'blockchainMetadata',
    factory: createBlockchainMetadata,
    // the container will pass `({ assetsModule, enabledWalletAccountsAtom })` to the factory
    dependencies: ['assetsModule', 'enabledWalletAccountsAtom'],
  },
])

Overriding dependencies

The container will throw when it encounters a duplicate dependency id. To override a dependency for test purposes, use the override flag

container.register({
  id: 'balances',
  override: true,
  factory: () => ({ load: jest.fn() }),
})

container.resolve()

const {
  storage,
  assetsModule,
  walletAccountsAtom,
  enabledWalletAccountsAtom,
  blockchainMetadata,
  balances,
} = container.getAll()

Optional dependencies

Optional dependencies can be requested by appending a trailing ?:

container.register({
  id: 'jediSurvivor',
  factory: ({ lightsaber, theForce }) => {
    if (lightsaber) {
      // code has to make sure optionality is gracefully handled
      return new InsaneLightsaberWiedlingJedi(lightsaber)
    }

    return new ThereIsOnlyTheForce(theForce)
  },
  dependencies: ['lightsaber?', 'theForce'],
})

Injection Style

By default dependencies are injected as named options, which is the recommended pattern. However, if your use case demands dependencies to be injected as positional arguments, e.g. if your factory function is a reselect selector, specify injectDependenciesAsPositionalArguments: true either at the container level via the constructor, or at the individual node level. See examples in tests.

Known Issues / Limitations

  • Only one instance of every node is ever created. In 99% of cases this is what you want but see Misc section for an example of getting around this.
  • This implementation conflates several concepts to one id:
    • The globally unique package id, as exported by a given package, e.g. 'analytics' for the analytics module.
    • The globally unique interface identifier, e.g. 'analytics', which corresponds to an object with certain methods with certain signatures. This is declared by consumers of analytics in their dependencies array of ids.
    • The constructor option name, for consumers of analytics, e.g. constructor({ analytics })

In the future we might disambiguate these, but in the short term we've opted for simplicity.

Example (headless)

See headless as a real-world e2e example that initializes an IOC and makes use of multiple preprocessors.

Rationale

Allow me to simulate your internal dialogue.

You: Holy crap! The afterlife is 1) more verbose, 2) has higher cognitive load and 3) breaks Intellisense by using ids instead of function pointers. Container: I hear you. I should improve those (help me help you). On the other hand, I have several advantages that grow as your list of nodes grows (e.g. the Exodus browser extension already has >100 nodes). I enable you to:

  • Define your dependencies declaratively.
  • Stop manually managing instantiation order. I'll sort things out by walking the dependency graph.
  • Unify instantiation patterns and best practices.
  • Validate that a node receives all declared dependencies and doesn’t receive any undeclared ones.
  • Centralize logging, error handling, permissions, etc. This lets you DRY up your code.
  • Track event/data propagation through the dependency tree.
  • Visualize/print/map out your dependency graph.
  • Auto-inject namespaced loggers, namespaced storage instances, and node-specific configuration en masse.

Misc (overachievers only)

Check out @exodus/dependency-preprocessors, which enables the config and logger special cases described above, and more.

版本与发布记录

@exodus/dependency-injection 共发布 23 个版本,首次发布于 2022/11/22;过去 12 个月发布了 2 个版本。

版本发布日期距今
3.3.1latest2026/10/141 个月前
3.3.02026/04/286 个月前
3.2.12025/08/131.3 年前
3.2.02025/01/241.8 年前
3.1.12024/11/022 年前
3.1.02024/09/262 年前

关于 @exodus/dependency-injection 的常见问题

@exodus/dependency-injection 是什么?

A simple dependency injection and inversion of container

如何安装 @exodus/dependency-injection?

在项目目录运行 npm install @exodus/dependency-injection,或使用 pnpm add / yarn add / bun add。

@exodus/dependency-injection 支持 TypeScript 吗?

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

@exodus/dependency-injection 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@exodus/dependency-injection 可以商用吗?

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

@exodus/dependency-injection 的下载量有多少?

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

@exodus/dependency-injection 还在维护吗?

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