
最近在 Bun 生态中尝试构建企业级应用时发现依赖注入DI框架的选择是个不大不小的痛点。虽然 Bun 本身性能卓越但周边生态尤其是面向大型应用架构的工具链相比 Node.js 仍处于快速发展阶段。许多团队习惯了 NestJS 那种清晰、声明式的依赖管理方式但直接引入 NestJS 又显得过于臃肿且reflect-metadata的运行时开销和配置复杂度在追求轻量高效的 Bun 项目中并不总是最优解。正是在这种背景下我注意到了社区新秀dunx。它宣称能为 Bun 应用提供类似 NestJS 风格的依赖注入体验但无需依赖reflect-metadata这立刻引起了我的兴趣。经过一段时间的实践和源码探究我发现dunx确实提供了一套简洁、高效且符合直觉的 DI 解决方案。本文将带你从零开始完整拆解dunx的核心概念、使用方法、实战案例并深入分析其实现原理与最佳实践无论你是 Bun 新手还是正在寻找轻量级 DI 方案的开发者都能从中获得可直接复用的知识。1. 背景与核心概念为什么需要dunx在深入代码之前我们有必要厘清几个关键问题什么是依赖注入NestJS 风格又指什么为什么在 Bun 中需要一个新的 DI 工具依赖注入Dependency Injection, DI是一种设计模式也是实现控制反转IoC的一种技术。它的核心思想是一个类不应该自己创建它所依赖的对象而是应该由外部容器通常是 IoC 容器来创建并“注入”给它。这样做的好处非常明显解耦类与它的依赖之间是松耦合的依赖的具体实现可以轻易替换。可测试性在单元测试中可以轻松注入模拟对象Mock。可维护性对象的创建和生命周期管理集中到容器中代码更清晰。NestJS 风格的 DI 特指 NestJS 框架所倡导和使用的一套基于装饰器Decorator和元数据Metadata的依赖注入体系。开发者通过Injectable()、Inject()等装饰器来声明类的可注入性及其依赖关系框架在启动时扫描这些元数据自动构建并管理整个依赖关系图。这种方式声明性强代码优雅极大地提升了开发体验。然而NestJS 的这套体系严重依赖reflect-metadata这个 polyfill 库来在运行时读取和写入 TypeScript 编译器生成的类型元数据。这带来了两个问题性能开销虽然不大但在高性能场景下额外的反射操作仍是一种开销。配置复杂度需要在tsconfig.json中开启emitDecoratorMetadata选项并且确保reflect-metadata在代码中最先被引入。Bun作为一个全新的 JavaScript 运行时以其出色的启动速度和运行性能著称。许多开发者选择 Bun 正是看中了其“轻快”的特性。在这种情况下引入一个需要reflect-metadata的完整框架可能违背了初衷。于是dunx应运而生。它的目标很明确在 Bun 环境中提供一套无需reflect-metadata但体验接近 NestJS的轻量级依赖注入库。它通过巧妙的设计利用 TypeScript 的类型系统和 Bun 的运行时特性实现了依赖的自动解析和注入让开发者既能享受 DI 带来的架构优势又能保持 Bun 应用的轻量与高效。2. 环境准备与项目初始化在开始编码前我们需要准备好开发环境。dunx是为 Bun 设计的因此 Bun 是必须的。2.1 安装 Bun如果你还没有安装 Bun可以通过以下命令安装以 macOS/Linux 为例# 使用官方安装脚本 curl -fsSL https://bun.sh/install | bash # 或者使用 npm是的可以用 npm 安装 bun npm install -g bun安装完成后验证安装bun --version # 输出类似bun v1.1.82.2 创建项目并安装dunx接下来我们创建一个新的 Bun 项目并安装dunx。# 1. 创建一个新目录并进入 mkdir bun-dunx-demo cd bun-dunx-demo # 2. 初始化 Bun 项目会生成 package.json 和 tsconfig.json bun init # 在交互式提示中你可以按需填写项目名、入口文件等。 # 为了演示我们可以全部按回车使用默认值。 # 3. 安装 dunx bun add dunx # 4. 安装 TypeScript 类型定义通常是开发依赖但 bun init 可能已包含 bun add -d typescript types/bun完成后的package.json应该类似于{ name: bun-dunx-demo, module: index.ts, type: module, devDependencies: { types/bun: latest, typescript: ^5.0.0 }, peerDependencies: { typescript: ^5.0.0 }, dependencies: { dunx: ^0.1.0 // 版本号以实际为准 } }2.3 配置 TypeScriptbun init生成的tsconfig.json通常已经配置好了。为了确保装饰器语法正常工作我们需要确认其中包含以下选项{ compilerOptions: { // ... 其他配置 experimentalDecorators: true, emitDecoratorMetadata: false, // 注意dunx 不需要这个 target: ES2022, module: ESNext, moduleResolution: bundler, // ... 其他配置 } }关键点emitDecoratorMetadata必须为false或者直接不设置默认就是false。这是dunx与需要reflect-metadata的库最大的不同之处也是其轻量的原因之一。3.dunx核心概念与 API 拆解dunx的 API 设计非常精简主要围绕以下几个核心概念展开3.1 容器 (Container)容器是dunx的核心它负责管理所有依赖称为“提供者”的生命周期和解析逻辑。你可以把它理解为一个高级的Map存储着类型标识符与如何创建其实例的工厂函数之间的映射关系。3.2 提供者 (Provider)提供者是能够被注入的“东西”。在dunx中一个提供者通常是一个类Class。你需要使用Injectable()装饰器来标记一个类是可注入的。3.3 作用域 (Scope)作用域定义了提供者实例的生命周期。dunx支持两种常见的作用域SINGLETON(默认)容器中只有一个共享实例。每次请求该依赖得到的都是同一个对象。TRANSIENT每次请求该依赖容器都会创建一个新的实例。3.4 装饰器 (Decorators)dunx提供了几个关键的装饰器来声明依赖关系Injectable(scope?): 标记一个类为可注入的提供者并可指定其作用域。Inject(token?): 在类的构造函数参数或属性上使用告诉容器需要注入哪个依赖。3.5 令牌 (Token)令牌是用于标识一个依赖的“钥匙”。它可以是类本身最常用。字符串或 Symbol用于注入非类依赖如配置对象、函数等。下面我们通过代码来具体理解这些概念。4. 完整实战案例构建一个简易用户服务我们将通过一个完整的例子模拟一个简单的后端服务包含用户仓库UserRepository、用户服务UserService和一个用于启动的应用App。4.1 项目结构首先创建以下目录和文件结构bun-dunx-demo/ ├── src/ │ ├── container.ts # 创建和配置容器 │ ├── user/ │ │ ├── user.entity.ts │ │ ├── user.repository.ts │ │ └── user.service.ts │ └── app.ts # 应用入口 ├── index.ts # 主文件启动应用 ├── package.json └── tsconfig.json4.2 定义实体与仓库src/user/user.entity.ts// 一个简单的用户实体 export interface User { id: number; name: string; email: string; } // 模拟一些初始数据 export const MOCK_USERS: User[] [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com }, ];src/user/user.repository.tsimport { Injectable, Scope } from dunx; import { User, MOCK_USERS } from ./user.entity; // 使用 Injectable 装饰器标记此类为可注入的提供者。 // 默认是 SINGLETON 作用域这里显式声明也无妨。 Injectable(Scope.SINGLETON) export class UserRepository { private users: User[] [...MOCK_USERS]; findAll(): User[] { return this.users; } findById(id: number): User | undefined { return this.users.find(user user.id id); } create(user: OmitUser, id): User { const newUser { id: this.users.length 1, ...user }; this.users.push(newUser); return newUser; } }关键解释Injectable()是必须的它告诉dunx这个类可以被容器管理。Scope.SINGLETON意味着整个应用中只有一个UserRepository实例。这对于无状态的仓库类来说是合理且高效的。4.3 创建业务服务src/user/user.service.tsimport { Injectable, Inject } from dunx; import { User } from ./user.entity; import { UserRepository } from ./user.repository; Injectable() // 默认也是 SINGLETON export class UserService { // 方式一通过构造函数注入推荐 // 使用 Inject() 装饰器参数是依赖的令牌Token这里直接传入类本身。 constructor( Inject(UserRepository) private readonly userRepo: UserRepository ) {} // 也可以不使用 Injectdunx 能通过 TypeScript 的类型信息在设计时和容器注册在运行时来解析。 // constructor(private readonly userRepo: UserRepository) {} getAllUsers(): User[] { // 这里可以添加业务逻辑比如权限检查、数据转换等 return this.userRepo.findAll(); } getUserById(id: number): User | undefined { return this.userRepo.findById(id); } registerUser(name: string, email: string): User { // 业务逻辑验证邮箱等 if (!email.includes()) { throw new Error(Invalid email format); } return this.userRepo.create({ name, email }); } }关键解释UserService依赖UserRepository。我们在构造函数参数上使用了Inject(UserRepository)。这是最明确的注入方式。实际上在dunx中如果参数类型是一个已经被注册为提供者的类你有时可以省略Inject()。但为了代码清晰和避免隐式行为带来的困惑我强烈建议始终显式使用Inject()尤其是在团队协作中。4.4 配置依赖注入容器这是dunx应用的核心配置环节。我们需要创建一个容器并告诉它有哪些提供者。src/container.tsimport { Container } from dunx; import { UserRepository } from ./user/user.repository; import { UserService } from ./user/user.service; // 1. 创建容器实例 export const container new Container(); // 2. 注册提供者 // 方法一使用 register 方法明确指定令牌和类。 container.register(UserRepository, UserRepository); container.register(UserService, UserService); // 方法二使用 registerClass更简洁令牌自动使用类本身。 // container.registerClass(UserRepository); // container.registerClass(UserService); // 3. 可选注册非类提供者例如一个配置对象 export const APP_CONFIG { port: 3000, env: development, } as const; // 使用字符串作为令牌 container.register(APP_CONFIG, APP_CONFIG); console.log(Dependency container has been configured.);关键解释Container是dunx导出的主类。register(token, providerClass)方法用于注册一个提供者。token是查找依赖的钥匙providerClass是具体的类。registerClass(providerClass)是便捷方法等价于register(providerClass, providerClass)。你可以注册任何类型的令牌和提供者包括值如上面的APP_CONFIG后续可以通过Inject(APP_CONFIG)来注入。4.5 构建应用入口src/app.tsimport { Injectable, Inject } from dunx; import { UserService } from ./user/user.service; // 我们的主应用类也由容器管理 Injectable() export class App { constructor( Inject(UserService) private readonly userService: UserService, Inject(APP_CONFIG) private readonly config: { port: number; env: string } ) {} async run() { console.log(App starting in ${this.config.env} mode on port ${this.config.port}...\n); // 1. 获取所有用户 const allUsers this.userService.getAllUsers(); console.log(All users:, allUsers); // 2. 根据ID查询用户 const user this.userService.getUserById(1); console.log(User with ID 1:, user); // 3. 注册新用户 try { const newUser this.userService.registerUser(Charlie, charlieexample.com); console.log(New user registered:, newUser); } catch (error) { console.error(Registration failed:, error.message); } // 4. 再次查询所有用户看是否新增 const updatedUsers this.userService.getAllUsers(); console.log(\nAll users after registration:, updatedUsers); } }4.6 启动应用最后在项目根目录的index.ts中我们从容器中解析出App实例并运行它。index.ts// 首先导入容器配置执行注册逻辑 import ./src/container; import { container } from ./src/container; import { App } from ./src/app; async function bootstrap() { // 从容器中解析 App 类的实例。 // 容器会自动处理其所有依赖UserService, APP_CONFIG // 以及依赖的依赖UserRepository。 const appInstance container.resolve(App); // 运行应用 await appInstance.run(); } bootstrap().catch(console.error);4.7 运行与验证现在一切就绪运行我们的应用bun run index.ts预期的输出应该类似于Dependency container has been configured. App starting in development mode on port 3000... All users: [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com } ] User with ID 1: { id: 1, name: Alice, email: aliceexample.com } New user registered: { id: 3, name: Charlie, email: charlieexample.com } All users after registration: [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com }, { id: 3, name: Charlie, email: charlieexample.com } ]恭喜你已经成功使用dunx构建了一个具有完整依赖注入层次结构的 Bun 应用。App-UserService-UserRepository的依赖链被容器自动处理得井井有条。5. 进阶用法与原理浅析5.1 循环依赖处理循环依赖A 依赖 BB 也依赖 A是 DI 容器中常见的问题。dunx如何处理呢让我们看一个例子注意循环依赖是糟糕的设计应尽量避免。// service-a.ts import { Injectable, Inject } from dunx; import { ServiceB } from ./service-b; Injectable() export class ServiceA { constructor(Inject(ServiceB) public b: ServiceB) {} } // service-b.ts import { Injectable, Inject } from dunx; import { ServiceA } from ./service-a; Injectable() export class ServiceB { constructor(Inject(ServiceA) public a: ServiceA) {} // 循环依赖 }如果你尝试container.resolve(ServiceA)很可能会得到一个运行时错误因为容器在创建ServiceA时需要ServiceB而创建ServiceB又需要ServiceA形成了死锁。dunx的解决方案是使用属性注入Property Injection作为变通方案或者使用前向引用Forward Ref。不过在dunx的当前版本中更鼓励通过代码设计如引入第三个服务、使用观察者模式等来避免循环依赖。如果确实无法避免可以尝试使用Inject配合Lazy包装或者手动在构造函数中延迟解析但这需要更精细的控制。最佳实践在项目初期就通过合理的模块划分来杜绝循环依赖。5.2 动态提供者与工厂模式有时依赖的创建逻辑很复杂不能简单地new一个类。这时可以使用工厂提供者。import { Container } from dunx; const container new Container(); // 注册一个工厂函数作为提供者 container.register(CONNECTION_POOL, { useFactory: () { // 复杂的初始化逻辑 const pool createDatabasePool({ host: process.env.DB_HOST, // ... 其他配置 }); pool.connect(); return pool; }, // 可以指定作用域例如 SINGLETON确保只创建一次连接池 scope: Scope.SINGLETON, }); // 在某个服务中注入 export class SomeService { constructor(Inject(CONNECTION_POOL) private pool: any) {} }5.3dunx如何实现无reflect-metadata的注入这是dunx最巧妙的地方。它并没有使用运行时类型反射那么Inject()装饰器里的参数令牌信息去哪了装饰器存储元数据Inject(token)装饰器在执行时会将token信息以Symbol 为键存储到被装饰参数所在类的元数据中。注意这个“元数据”是装饰器自身利用WeakMap或类似结构在内存中维护的一个映射表不是Reflect.defineMetadata那种标准的反射元数据。这完全是在用户代码层面实现的不依赖 polyfill。容器解析当container.resolve(SomeClass)被调用时容器会 a. 检查SomeClass是否已注册。如果已注册获取其构造函数。 b. 查看构造函数上是否有通过Inject()存储的参数令牌信息。 c. 根据这些令牌递归地解析每一个依赖。 d. 使用解析好的依赖实例通过new SomeClass(...deps)来创建目标实例。简单来说dunx用自己维护的一套内部映射关系替代了reflect-metadata提供的通用反射能力从而实现了轻量化和对 Bun 环境的零负担适配。6. 常见问题与排查思路在实际使用dunx时你可能会遇到以下问题问题现象可能原因解决思路Error: No provider found for token: [object Object]1. 依赖的类没有使用Injectable()装饰。2. 依赖的类没有在容器中注册register或registerClass。3.Inject()中使用的令牌与注册时的令牌不匹配例如用了字符串令牌但注册时用了类。1. 检查所有需要被注入的类确保都有Injectable()。2. 检查container.ts或容器配置处确保所有依赖都已正确注册。对于嵌套依赖其依赖的类也需要注册。3. 确保注入令牌的唯一性和一致性。使用类作为令牌是最推荐的方式。得到的依赖实例是undefined或null1. 可能注册了一个undefined的值。2. 作用域为TRANSIENT的类在某个环节没有被正确实例化。3. 循环依赖导致实例化失败。1. 检查工厂函数或值注册的逻辑。2. 检查TRANSIENT作用域的提供者确保每次resolve的逻辑正确。3. 检查是否存在循环依赖并重构代码。装饰器语法报错Decorators are not valid here1.tsconfig.json中未启用experimentalDecorators: true。2. 装饰器用在了错误的地方如非类、非方法、非属性上。1. 确认tsconfig.json配置正确。2. 确保Injectable()用在类上Inject()用在构造函数参数或属性上。程序运行正常但依赖似乎没有注入参数为undefined1. 在可以省略Inject()的情况下类型信息可能因为打包或编译过程丢失导致容器无法推断令牌。2. 构造函数参数使用了联合类型等复杂类型。始终显式使用Inject(令牌)。这是最可靠的做法可以完全避免因类型推断失败导致的问题。7. 最佳实践与工程建议将dunx用于实际项目时遵循以下实践能让你的代码更健壮、更易维护模块化组织提供者不要把所有register调用堆在一个文件里。可以按功能模块创建多个container.config.*.ts文件然后在主容器文件中导入并执行它们。// container.config.user.ts import { Container } from dunx; import { UserRepository, UserService } from ./user; export function configureUserModule(container: Container) { container.registerClass(UserRepository); container.registerClass(UserService); } // main-container.ts import { Container } from dunx; import { configureUserModule } from ./config/container.config.user; import { configureProductModule } from ./config/container.config.product; const container new Container(); configureUserModule(container); configureProductModule(container);始终使用显式Inject()尽管dunx在某些情况下能进行推断但显式声明依赖关系能使代码的意图一目了然减少歧义并且对构建工具更友好。优先使用构造函数注入相比于属性注入构造函数注入能更清晰地声明类的必需依赖并且可以确保类在实例化后立即处于完全可用状态因为依赖在构造函数中就已就绪。为非类依赖使用字符串或 Symbol 令牌对于配置对象、常量、外部库实例等使用独特的字符串或 Symbol 作为令牌避免与类令牌冲突。export const CONFIG_TOKEN Symbol.for(AppConfig); container.register(CONFIG_TOKEN, { apiUrl: ... }); // 注入时 constructor(Inject(CONFIG_TOKEN) private config: any) {}注意作用域的选择SINGLETON用于无状态服务、配置、数据库连接池、HTTP 客户端等。这是默认且最常用的选项。TRANSIENT用于每次请求都需要新实例的场景例如可能包含请求特定状态的工厂、处理器等。在 Web 服务器中处理每个请求时如果需要隔离可以考虑使用。在应用根目录创建容器通常一个应用应该只有一个根容器。在应用启动文件如index.ts、main.ts中创建并配置容器然后将其作为“服务定位器”的替代品在需要的地方导入使用或者通过顶层框架如你自定义的 HTTP 服务器向下传递。编写单元测试DI 的一大优势就是便于测试。你可以为容器创建一个测试版本用模拟对象Mock替换真实的提供者。// 测试中 const testContainer new Container(); testContainer.register(UserRepository, MockUserRepository); // 注入模拟仓库 const testService testContainer.resolve(UserService); // 现在可以对 testService 进行测试它使用的是 MockUserRepositorydunx为 Bun 生态系统带来了一个非常优雅且实用的依赖注入解决方案。它抓住了 NestJS DI 核心体验的精髓——声明式、装饰器驱动同时果断摒弃了reflect-metadata带来的包袱完美契合了 Bun 运行时追求性能和简洁的理念。通过本文的梳理你应该已经掌握了从环境搭建、核心概念理解、到完整项目实战的整个流程。dunx的 API 简洁而强大足以支撑起中小型 Bun 应用的架构需求。当然对于超大型项目你可能还需要评估其与更成熟框架如 NestJS for Bun的整合能力但dunx无疑为那些希望保持轻量、又需要良好架构的 Bun 开发者提供了一个绝佳的选择。