问题
很多时候
Controller需要ServiceService需要RepositoryRepository需要DataSourceDataSource需要Config
这叫 依赖图(dependency graph) 。你如果不用任何框架,通常会写成:
// ❌ 手动组装:依赖层层传递,创建顺序必须正确
const config = new Config(process.env)
const ds = new DataSource(config)
const repo = new UserRepository(ds)
const service = new UserService(repo)
const controller = new UserController(service)
问题会越来越明显:
- 创建顺序必须对(Config → DataSource → Repo → Service → Controller)
- 到处传来传去(每加一个依赖,构造函数都要改)
- 不好测试(想测 Service 还得造一堆依赖,或写一堆 fake)
- 生命周期难管(DataSource 要复用单例?要连接池?什么时候关闭?)
3种常见做法
A:最直观的“手写 DI 容器”(推荐你先看懂这个)
核心思想:把“怎么创建对象”集中放到一个容器里,业务代码只管“要什么”。
class Container {
constructor() {
this.factories = new Map()
this.singletons = new Map()
}
register(name, factory, { singleton = true } = {}) {
this.factories.set(name, { factory, singleton })
}
get(name) {
if (this.singletons.has(name)) return this.singletons.get(name)
const item = this.factories.get(name)
if (!item) throw new Error(`Not registered: ${name}`)
const instance = item.factory(this)
if (item.singleton) this.singletons.set(name, instance)
return instance
}
}
// ---- 业务对象(只声明依赖,不负责 new 下游)----
class Config {
constructor(env) { this.env = env }
get dbUrl() { return this.env.DB_URL }
}
class DataSource {
constructor(config) { this.config = config }
query(sql) { return `query(${sql}) @ ${this.config.dbUrl}` }
}
class UserRepository {
constructor(ds) { this.ds = ds }
findById(id) { return this.ds.query(`select * from user where id=${id}`) }
}
class UserService {
constructor(repo) { this.repo = repo }
getUser(id) { return this.repo.findById(id) }
}
class UserController {
constructor(service) { this.service = service }
handle(req) { return this.service.getUser(req.params.id) }
}
// ---- 依赖组装集中在一处 ----
const container = new Container()
container.register('config', () => new Config(process.env))
container.register('dataSource', c => new DataSource(c.get('config')))
container.register('userRepo', c => new UserRepository(c.get('dataSource')))
container.register('userService', c => new UserService(c.get('userRepo')))
container.register('userController', c => new UserController(c.get('userService')))
// 使用时:直接要 Controller
const controller = container.get('userController')
console.log(controller.handle({ params: { id: 1 } }))
你会发现:
- 创建顺序不再散落在各处,而是集中在容器注册区
- 依赖新增/替换(比如换 repo)只改注册,不改业务类
- 单例(DataSource)统一管理
B:工厂函数 + 显式依赖(更轻量,适合小项目)
把 new 都放到一个 createApp() 里:
function createApp(env) {
const config = new Config(env)
const ds = new DataSource(config)
const repo = new UserRepository(ds)
const service = new UserService(repo)
const controller = new UserController(service)
return { controller }
}
const { controller } = createApp(process.env)
它比到处 new 好,因为依赖组装集中,但不如容器灵活(替换/多实例/生命周期管理没那么方便)
C:使用成熟框架的 DI(NestJS / Spring 这类)
- 你只“声明依赖”(构造函数要什么)
- 容器负责“创建对象并把依赖塞进去”
- 容器也负责生命周期(单例、请求级、关闭钩子)
以 NestJS 的思路(示意)就是:
@Controller()里注入UserService@Injectable()的UserService注入UserRepositoryUserRepository再注入DataSource/Config
(框架帮你自动按依赖图排序创建)
比如这样声明 AppController 依赖了这两个 Service,然后让工具分析依赖自动帮我创建好这三个对象并设置依赖关系
这就是 IoC 的实现思路
有一个放对象的容器,程序初始化的时候会扫描 class 上声明的依赖关系,然后把这些 class 都给 new 一个实例放到容器里。
创建对象的时候,还会把它们依赖的对象注入进去。这样就完成了自动的对象创建和组装
Nest具体如何做的
有一个 AppService 声明了 @Injectable,代表这个 class 可注入,那么 nest 就会把它的对象放到 IOC 容器里
AppController 声明了 @Controller,代表这个 class 可以被注入,nest 也会把它放到 IoC 容器
AppController 的构造器参数依赖了 AppService。
或者这样通过属性的方式声明依赖:
前者是构造器注入,后者是属性注入,两种都可以。
为什么 Controller 是单独的装饰器呢?
因为 Service 是可以被注入也是可以注入到别的对象的,所以用 @Injectable 声明。
而 Controller 只需要被注入,所以 nest 单独给它加了 @Controller 的装饰器。
然后在 AppModule 里引入:
通过 @Module 声明模块,其中 controllers 是控制器,只能被注入。
providers 里可以被注入,也可以注入别的对象,比如这里的 AppService
入口模块使用
接下来 nest 会从 AppModule 开始解析 class 上通过装饰器声明的依赖信息,自动创建和组装对象
AppController 只是声明了对 AppService 的依赖,就可以调用它的方法了:
nest 在背后自动做了对象创建和依赖注入的工作
nest 还加了模块机制,可以把不同业务的 controller、service 等放到不同模块里,这个之前提过
比如
nest g module moduleA
生成这个模块代码
并更新了 app.module.ts 更新了这个 imports
如果生成一个 service
nest g service serviceA
生成代码
会添加到 providers 中
仓库