NestJS 后端开发 / 第 11 课
Provider 与依赖注入:让控制器调用服务
Nest 如何登记、创建并复用服务,并用构造函数把它交给控制器。
上一课已经见过 AppModule 中的 controllers 和 providers。这一课沿用同一个便笺(notes)小应用:控制器处理 HTTP 请求,服务负责便笺数据。先写清楚两者的分工,再看 Nest 如何替我们创建并连接对象。
1. 把工作放到服务里
服务是应用中的普通类。示例 NotesService 暂时把便笺存在内存数组中;重启服务后数据会清空,这样先专注于请求与代码结构,不需要数据库。
@Injectable()
export class NotesService {
private notes: Note[] = [];
findAll(tag?: string) {
return tag ? this.notes.filter((note) => note.tag === tag) : this.notes;
}
create(title: string, tag: string) {
const note = { id: String(this.notes.length + 1), title, tag };
this.notes.push(note);
return note;
}
}
上面的服务代码是简化摘录,完整的类型定义和 ID 生成在下载项目中。private 表示这份数组只由服务内部管理;filter 挑出满足条件的元素,push 把新元素放到数组末尾。findAll 查找数据,create 创建并保存数据。方法名是我们设计的,不是 Nest 强制要求的。@Injectable() 让 Nest 知道这个类可以由依赖注入容器管理;暂时把它理解为“可交给 Nest 创建和提供”的标记。
2. 控制器依赖服务
控制器声明路由,接收参数后把业务工作交给服务。Nest 会读取构造函数需要的类型,并找到对应的 provider:
@Controller('notes')
export class NotesController {
constructor(private readonly notes: NotesService) {}
@Get()
findAll(@Query('tag') tag?: string) {
return this.notes.findAll(tag);
}
}
constructor 是对象创建时执行的方法;这里声明控制器需要一个 NotesService。Nest 创建控制器时会创建或取得服务实例,并把它传进来,这叫依赖注入(Dependency Injection,简称 DI)。控制器不必自己写 new NotesService()。constructor(private readonly notes: NotesService) 是 TypeScript 的简写,同时声明字段并保存传入的参数;readonly 表示不能再把这个字段换成另一个实例。字段 this.notes 保存注入的服务,因此方法中能调用 this.notes.findAll(...)。@Controller('notes') 与 @Get() 把方法连到 GET /notes;查询参数 ?tag=study 由 @Query('tag') 取得。
3. Provider 要在模块中登记
AppModule 把 NotesController 放进 controllers,把 NotesService 放进 providers。Nest 才能在创建控制器时找到它的依赖。漏掉 provider 时,应用启动阶段就会报“无法解析依赖”,而不是等到某个请求到达才临时猜测。
如果控制器与服务属于不同模块,被使用的服务通常要由所属模块导出,使用方模块导入该模块。我们当前只有一个根模块,因此还不需要拆模块。先掌握“provider 登记在模块中、控制器通过构造函数声明需要它”这条链。
4. 生命周期与复用
Nest 默认 provider 是单例作用域:在一个应用实例中,Nest 创建一个服务实例,并在多个请求间复用。于是内存便笺数组会持续存在,后续请求能读到先前 POST 创建的数据。控制器通常也是由 Nest 管理的单例。请求作用域 provider 则可以按请求创建,但会增加创建成本,并影响依赖它的对象;初学阶段先不需要它。
单例并不意味着数据永久保存。进程重启后,内存数组消失;多个服务进程也不会自动共享数组。真实 OJ 会用数据库或其他持久存储保存用户、题目和提交记录,不能把这个练习服务当生产系统。
自己试一试
先启动示例,发 POST /notes 创建便笺,然后访问 GET /notes。如果把控制器里的 new NotesService() 换成每次请求都创建新服务,为什么列表可能为空?
**答案:**每个新实例有自己的空数组,POST 与 GET 操作的不是同一份状态。Nest 默认复用 provider 实例,因此这个小例子可共享内存数据。下一课将把路径参数、查询、请求正文与 Express 中间件做对照,并看请求经过 Nest 各阶段的顺序。
互动练习:让 Nest 找到服务依赖
NotesController 的构造函数需要 NotesService,但 AppModule 只登记了 controller。启动提示无法解析依赖,首先应检查什么?
Provider 是由 Nest 管理的应用对象;模块登记 provider,控制器通过构造函数依赖注入它。