10.2 手动 DI vs DI 容器
| 方式 | 说明 | 适合场景 |
|---|---|---|
| 手动 DI | 自己 new 实例,手动传参 | 中小项目,模块不多 |
| DI 容器 | 用 TypeDI / InversifyJS 等框架自动注入 | 大型项目,模块很多 |
本教程用手动 DI,够用且简单,不用引入额外依赖。
10.3 bootstrap:统一组装所有依赖
创建 src/bootstrap.ts,在这里把所有 Model、Service、Controller 都创建好,组装在一起。
import { projectData } from './data/project/index.js';
import { userData } from './data/user/index.js';
import { ProjectModel } from './models/project/project-model.js';
import { UserModel } from './models/user/user-model.js';
import { ProjectService } from './services/project/project-service.js';
import { UserService } from './services/user/user-service.js';
import { ProjectController } from './controllers/project/project-controller.js';
import { UserController } from './controllers/user/user-controller.js';
// 所有 Model 的集合
export interface AppModels {
projects: ProjectModel;
users: UserModel;
}
// 所有 Controller 的集合
export interface AppControllers {
project: ProjectController;
user: UserController;
}
// 所有东西都在这里组装
export function bootstrap() {
// ===== 1. Model 层 =====
const projects = new ProjectModel(projectData);
const users = new UserModel(userData);
const models: AppModels = { projects, users };
// ===== 2. Service 层 =====
const projectService = new ProjectService(projects);
const userService = new UserService(users, projects); // 用户 Service 可能依赖项目 Model
// ===== 3. Controller 层 =====
const project = new ProjectController(projectService);
const user = new UserController(userService);
const controllers: AppControllers = { project, user };
// ===== 4. 返回全部 =====
return { models, controllers };
}
然后在路由里用:
// src/routers/index.ts
import { Router } from 'express';
import type { AppControllers } from '../bootstrap.js';
import { createProjectRouter } from './project/project-router.js';
import { createUserRouter } from './user/user-router.js';
export function createRouter(controllers: AppControllers) {
const router = Router();
router.use('/projects', createProjectRouter(controllers.project));
router.use('/users', createUserRouter(controllers.user));
return router;
}
App 里调用 bootstrap:
// src/app.ts
import { bootstrap } from './bootstrap.js';
export function createApp() {
const { controllers } = bootstrap();
const app = express();
app.use(express.json());
app.use('/api/v1', createRouter(controllers));
// ...
return app;
}
10.4 启动全流程
从 node server.ts 到能接受请求,完整流程是这样的:
1. 执行 server.ts
↓
2. import app.ts → import bootstrap
↓
3. bootstrap() 执行
├─ 创建所有 Model(加载初始数据)
├─ 创建所有 Service(注入 Model)
└─ 创建所有 Controller(注入 Service)
↓
4. createApp() 执行
├─ 创建 express 实例
├─ 注册全局中间件
├─ 挂载路由(传入 controllers)
└─ 注册 404 和错误处理中间件
↓
5. app.listen() 开始监听端口
↓
6. 服务器启动完成,等待请求
请求到来时的流程
请求到达
↓
全局中间件(json 解析、日志、鉴权...)
↓
路由匹配(找到对应的 Controller 方法)
↓
Controller 解析参数 → 调用 Service
↓
Service 执行业务逻辑 → 调用 Model
↓
Model 操作数据 → 返回结果
↓
Service 返回结果给 Controller
↓
Controller 组装响应 → 返回给客户端
↓
(如果中间任何地方 throw 了错误 → 全局错误处理中间件捕获 → 返回错误响应)
10.5 为什么用函数而不是单例
你可能会想:直接导出单例不行吗?
// 单例写法
export const projectService = new ProjectService(projectModel);
这样写简单,但有问题:
- 测试不方便:每个测试用例想用不同的初始数据,做不到
- 有副作用的 import:import 这个文件就会执行 new,可能产生意料之外的行为
- 循环依赖难排查:A import B,B import A,单例模式下很容易出问题
用 bootstrap() 函数创建实例的好处:
- 调用时才创建,有明确的初始化时机
- 可以调用多次,每次创建全新的实例(测试很有用)
- 依赖关系集中在一个地方,一目了然
10.6 数据初始化
bootstrap 里除了组装依赖,还可以做数据初始化。
export function bootstrap() {
const projects = new ProjectModel(projectData);
const users = new UserModel(userData);
// 数据初始化:确保管理员用户存在
seedAdminUser(users);
// 数据一致性校验:检查引用完整性
validateDataConsistency({ projects, users });
return { projects, users };
}
function seedAdminUser(users: UserModel) {
if (!users.findByUsername('admin')) {
users.create({ username: 'admin', password: 'admin123', role: 'admin' });
}
}
10.7 小结
- 依赖注入:依赖从外面传进来,不要自己 new
- 中小项目用手动 DI 就够了,不用上 DI 容器
bootstrap.ts统一组装所有依赖,结构清晰- 启动流程:bootstrap 组装 → createApp 注册中间件和路由 → 监听端口
- 用工厂函数(bootstrap)而不是单例,更灵活、好测试