10 — 依赖注入与启动流程

0 阅读4分钟

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);

这样写简单,但有问题:

  1. 测试不方便:每个测试用例想用不同的初始数据,做不到
  2. 有副作用的 import:import 这个文件就会执行 new,可能产生意料之外的行为
  3. 循环依赖难排查: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)而不是单例,更灵活、好测试