上一篇:01 从 main.ts 看服务如何启动 | 下一篇:03 一个请求的完整生命周期
本篇学习入口
| 类型 | 内容 |
|---|---|
| 官方概念 | Modules、Providers:模块组织依赖,Provider 被容器创建和注入 |
| 小满知识点 | IOC、DI、Provider、Module 的 imports/providers/controllers |
| 本项目代码入口 | backend/oj-nest/src/app.module.ts、src/modules/question/question.module.ts、src/modules/question/question.service.ts |
四件套
NestJS 业务代码围绕四个角色组织:
flowchart TB Mod[Module模块] --> Ctrl[Controller控制器] Mod --> Svc[Service服务] Mod --> Prov[Provider提供者] Ctrl -->|调用| Svc Svc -->|注入| Repo[Repository等]
| 角色 | 职责 | 前端类比 |
|---|---|---|
| Module | 把相关功能打包、声明依赖 | Vue 子组件 + defineOptions |
| Controller | 定义 HTTP 路由,接收请求 | Vue Router 的 route + 页面只负责接参 |
| Service | 业务逻辑、调数据库/Redis | composable / pinia action |
| Provider | 可被注入的类(Service 也是 Provider) | 可复用的逻辑单元 |
知识点:IOC 是“对象由框架容器管理”,DI 是“依赖由容器注入进来”。在 Nest 里,Service、Repository、Queue 这类对象通常都交给容器管理。
前端类比:组件里直接
importcomposable 是手动拿依赖;Nest 通过 constructor 声明依赖,由框架传入实例。本项目落点:
QuestionService不自己创建 Repository 和 Queue,而是在构造函数里声明,由QuestionModule配好后自动注入。
以 QuestionModule 为例
文件:backend/oj-nest/src/modules/question/question.module.ts
|
|
解读:
- imports — 这个模块需要的外部能力(数据库表、Bull 队列)
- controllers — 对外暴露的 HTTP 接口(管理员 + 用户两套路由)
- providers — 内部业务逻辑类,Nest 容器会实例化并管理
知识点:Module 是依赖边界。
imports让当前模块获得外部能力,controllers注册路由入口,providers注册可注入对象。前端类比:一个业务模块同时声明自己要用哪些 store、有哪些页面入口、有哪些可复用逻辑。
本项目落点:
QuestionModule声明了题目相关 Entity、判题队列、管理员 Controller、用户 Controller 和QuestionService。
Controller:路由层
文件:backend/oj-nest/src/modules/question/user-question.controller.ts
|
|
@Controller('api/user/testQuestion')— 路由前缀@Get('getTestQuestionById')— 完整路径GET /api/user/testQuestion/getTestQuestionById@Query('id')— 取 URL 查询参数?id=xxx@Body()— 取 POST JSON body@UseGuards(TokenGuard)— 整个 Controller 都要登录
Controller 不写业务逻辑,只做三件事:接参、调 Service、返回 Promise。
知识点:Controller 负责把 HTTP 世界转换成函数调用,例如从 query、body、header 取参数。
前端类比:页面组件不应该塞满请求和业务细节,而是把复杂逻辑交给 composable 或 store。
本项目落点:
UserQuestionController只接收题目查询和提交请求,真正的题目读取、提交记录保存、队列投递都在QuestionService。
Service:业务层
文件:backend/oj-nest/src/modules/question/question.service.ts
|
|
@Injectable()— 告诉 Nest「这个类可以被注入」- 构造函数里声明依赖 — Nest 自动帮你 new 好传进来
知识点:Provider 是 Nest 容器能管理的对象,
@Injectable()是最常见的 Provider 声明方式。前端类比:Service 像一个可复用业务模块,但它的依赖不是自己 import 后 new,而是由容器注入。
本项目落点:
QuestionService同时依赖多个 Repository 和 Bull Queue,依赖关系清楚写在 constructor 里。
依赖注入(DI)原理
前端常见写法:
|
|
NestJS 写法:
|
|
flowchart LR Nest[Nest容器] -->|实例化| QS[QuestionService] Nest -->|注入| QC[UserQuestionController] QC -->|constructor| QS
好处:
- 不用自己 new — 生命周期由框架管
- 单例默认 — 同一 Module 内 Service 共享一个实例
- 易测试 — 测试时可 mock 注入的依赖
- 模块边界清晰 — imports 声明「我用谁」
知识点:DI 的核心不是“少写 new”,而是让依赖关系显式、可替换、由框架统一管理生命周期。
前端类比:把全局单例散落在各处 import 会越来越难替换;DI 把依赖集中在模块和构造函数里。
本项目落点:Redis、TypeORM Repository、Bull Queue 都通过模块注册后注入,业务类不用关心底层连接怎么创建。
项目模块划分
flowchart TB App[AppModule] --> User[UserModule] App --> Question[QuestionModule] App --> Comment[CommentModule] App --> Judge[JudgeModule] Question -->|投递任务| Judge Judge -->|消费队列| J0[Judge0]
| 模块 | Controller | Service | 特殊 |
|---|---|---|---|
| UserModule | user.controller | user.service | Redis 存 token |
| QuestionModule | admin + user 两个 controller | question.service | 注册 Bull 队列 |
| CommentModule | comment.controller | comment.service | 嵌套评论 |
| JudgeModule | 无 | judge.processor | 纯后台 Worker |
JudgeModule 没有 Controller — 它不对外提供 HTTP,只消费 Redis 里的 Bull 队列任务。
TypeOrmModule.forFeature
|
|
在 Module 里注册「这个模块要用哪些表」,之后 Service 里才能 @InjectRepository(Questions)。
类比:在组件里 import 某个 store,才能 useXxxStore()。
知识点:
forFeature是 TypeORM 和 Nest 模块系统的连接点,它把指定 Entity 的 Repository 注册到当前模块。前端类比:只在当前业务域里引入需要的 store,避免所有模块都拿到所有状态。
本项目落点:
QuestionModule注册题目、测试点、提交代码、提交记录四类 Entity,所以QuestionService能注入这些 Repository。
两个 Controller 共用一个 Service
QuestionModule 有:
AdminQuestionController→api/testQuestion— 增删改UserQuestionController→api/user/testQuestion— 查 + 提交
两者注入同一个 QuestionService,逻辑复用、路由分离。类似 admin 路由和普通用户路由共用一套 API 层。
自定义装饰器
文件:backend/oj-nest/src/common/decorators/current-token.decorator.ts
|
|
Guard 校验通过后把 token 挂到 request['token'],Controller 用 @CurrentToken() 直接取 — 不用每个方法重复解析 header。
本篇小结
| 你要写的 | 放哪里 | 装饰器 |
|---|---|---|
| 路由 | Controller | @Controller @Get @Post |
| 业务 | Service | @Injectable() |
| 打包 | Module | @Module({ imports, controllers, providers }) |
| 鉴权 | Guard 挂 Controller | @UseGuards(TokenGuard) |
| 取参 | Controller 方法参数 | @Body @Query @Headers |
下一篇跟踪一个真实请求:从 axios 发 login 到拿到 JSON 的全链路。