仓库结构
计划给出了一个目标目录结构和几条命名约定。本页把它们和 TaskFlow 的实际目录放在一起对照,解释差异,并详细说明这些 约定。它们不只是为了好看:好几个工具是按文件后缀来挑选文件的。
目录结构
计划中的目标结构
text
taskflow/
├── docs/
│ └── PLAN.md # 本文档
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── common/ # filters, interceptors, pipes, decorators
│ ├── config/
│ ├── auth/
│ ├── users/
│ ├── projects/
│ ├── tasks/
│ ├── notifications/
│ └── database/
│ └── migrations/
├── test/ # e2e
├── docker-compose.yml
├── Dockerfile
├── .env.example
├── LEARNINGS.md
└── README.mdTaskFlow 的实际结构
项目在 examples/ 下(不含 node_modules/、dist/ 和 coverage/,它们是生成出来的,被项目自己的 .gitignore 忽略):
text
examples/
├── src/
│ ├── main.ts 启动:NestFactory.create
│ ├── app.module.ts 根模块、全局 provider、中间件
│ ├── app.setup.ts main.ts 与端到端测试共用的 HTTP 设置、Swagger
│ ├── common/
│ │ ├── common.module.ts 全局模块,CLOCK token
│ │ ├── correlation-id.ts
│ │ ├── decorators/auth.decorators.ts @Public(), @Roles(), @CurrentUser()
│ │ ├── dto/pagination.dto.ts PaginationQueryDto, Page<T>
│ │ ├── filters/all-exceptions.filter.ts (+ .spec.ts)
│ │ ├── interceptors/logging.interceptor.ts (+ .spec.ts)
│ │ ├── middleware/correlation-id.middleware.ts
│ │ └── time-zone.ts APP_TIME_ZONE, localDate()
│ ├── config/
│ │ ├── configuration.ts 类型化配置 (registerAs)
│ │ └── env.validation.ts 环境变量 schema (+ .spec.ts)
│ ├── database/
│ │ ├── database.module.ts TypeOrmModule.forRootAsync
│ │ ├── postgres-options.ts 应用和 CLI 共用的选项
│ │ ├── data-source.ts TypeORM CLI 的 DataSource
│ │ ├── seed.ts 开发用种子数据
│ │ └── migrations/1793093891613-InitialSchema.ts
│ ├── auth/
│ │ ├── auth.module.ts, auth.controller.ts, auth.service.ts (+ .spec.ts)
│ │ ├── jwt.strategy.ts
│ │ ├── guards.ts JwtAuthGuard, RolesGuard (+ .spec.ts)
│ │ └── dto/auth.dto.ts
│ ├── users/
│ │ ├── users.module.ts, users.controller.ts, users.service.ts (+ .spec.ts)
│ │ ├── role.enum.ts
│ │ └── entities/user.entity.ts
│ ├── projects/
│ │ ├── projects.module.ts, projects.controller.ts, projects.service.ts (+ .spec.ts)
│ │ ├── projects.store.ts ProjectsStore 接口 + PROJECTS_STORE token
│ │ ├── in-memory-projects.store.ts J2-J3 的实现
│ │ ├── typeorm-projects.store.ts J4 起的实现,含事务
│ │ ├── dto/project.dto.ts
│ │ └── entities/project.entity.ts, project-member.entity.ts
│ ├── tasks/
│ │ ├── tasks.module.ts, tasks.controller.ts, tasks.service.ts (+ .spec.ts)
│ │ ├── tasks.scheduler.ts OVERDUE 定时任务
│ │ ├── task-status.enum.ts
│ │ ├── dto/task.dto.ts
│ │ ├── entities/task.entity.ts
│ │ └── events/task-status-changed.event.ts
│ ├── notifications/
│ │ ├── notifications.module.ts
│ │ ├── notifications.listener.ts @OnEvent
│ │ └── notifications.queue.ts BullMQ 模块和 processor
│ ├── health/health.controller.ts Terminus (+ HealthModule)
│ └── demo/event-loop.controller.ts J1 的练习 (+ DemoModule),生产环境不加载
├── test/
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── docker-compose.yml
├── Dockerfile, .dockerignore, .gitignore
├── .gitlab-ci.yml
├── .env.example
├── .oxlintrc.json, nest-cli.json, jest.config.ts, tsconfig.json, tsconfig.build.json
├── package.json, package-lock.json
├── LEARNINGS.md
└── README.md差异及原因
| 计划 | TaskFlow | 原因 |
|---|---|---|
docs/PLAN.md | 没有 | 计划发布在本网站上 |
common/:filters、interceptors、pipes、decorators | common/:filters、interceptors、middleware、decorators、dto | 内置管道(ParseUUIDPipe、ParseEnumPipe)就够用;追踪 ID 中间件和分页 DTO 是共用的 |
database/migrations/ | 另有 database.module.ts、postgres-options.ts、data-source.ts、seed.ts | TypeORM CLI 不启动 Nest:它有自己的 DataSource,和应用共用一套选项(J5) |
| — | app.setup.ts | 不属于 provider 的 HTTP 设置(版本、helmet、CORS)同时供 main.ts 和端到端测试使用 |
| — | health/、demo/ | J11 的 Terminus 健康检查;J1 的事件循环练习,生产环境不加载 |
test/(端到端) | test/ 放端到端测试,单元测试放在代码旁边(*.spec.ts) | Nest CLI 的约定 |
| 根目录文件 | 另有 .gitlab-ci.yml、.oxlintrc.json、.dockerignore、.gitignore、nest-cli.json、jest.config.ts | J11 的 CI、lint、镜像构建上下文、不进 git 的文件、Swagger 插件、覆盖率阈值 |
create-project.dto.ts(约定示例) | dto/project.dto.ts 集中放一个模块的 DTO | 一个模块的 DTO 都很短(项目四个,任务两个):放在一个文件里依然好读。nest g resource 会每个 DTO 生成一个文件,两种做法都说得过去 |
约定
每个领域一个模块
每个领域(用户、项目、任务……)都有自己的模块,声明自己的控制器和 provider,只导出别的模块需要的东西。这相当于 Spring 里的“按功能分包”(package by feature),但有一个很大的区别:什么都不会被自动发现,模块之间的每个依赖都要 显式写进 imports(J3)。
| 模块 | 文件 | 导出 | 被谁导入 |
|---|---|---|---|
CommonModule | common/common.module.ts | CLOCK(@Global() 模块) | 全局可见 |
DatabaseModule | database/database.module.ts | —(配置 TypeORM) | AppModule |
UsersModule | users/users.module.ts | UsersService | AuthModule |
AuthModule | auth/auth.module.ts | — | AppModule |
ProjectsModule | projects/projects.module.ts | ProjectsService(不导出 store) | TasksModule |
TasksModule | tasks/tasks.module.ts | — | AppModule |
NotificationsModule | notifications/notifications.module.ts | — | AppModule;定义了 REDIS_URL 时导入 NotificationsQueueModule |
HealthModule | health/health.controller.ts | — | AppModule |
DemoModule | demo/event-loop.controller.ts | — | AppModule,生产环境不加载 |
只包含一个控制器的模块(HealthModule、DemoModule)和这个控制器写在同一个文件里;模块一旦有多个 provider,就有 自己的 *.module.ts。
dto/ 和 entities/ 目录
- 每个接收数据的模块都有
dto/(projects/dto、tasks/dto、auth/dto),共用的部分放在common/dto/(分页)。 entities/放在拥有这张表的模块里:ProjectMember在projects/entities下,因为项目成员由项目模块管理。- 有需要时才加别的子目录:
tasks/events/放事件类,common/里按组件类型各一个目录(filters/、interceptors/、middleware/、decorators/)。
文件和类的命名
文件名用 kebab-case 并以类型作后缀,类名用 PascalCase 并带同样的后缀:
| 元素 | 文件 | 类 |
|---|---|---|
| 模块 | projects.module.ts | ProjectsModule |
| 控制器 | projects.controller.ts | ProjectsController |
| Service | projects.service.ts | ProjectsService |
| 实体 | project.entity.ts、project-member.entity.ts | Project、ProjectMember |
| DTO | create-project.dto.ts(计划、nest g)或 project.dto.ts(TaskFlow) | CreateProjectDto |
| 过滤器 | all-exceptions.filter.ts | AllExceptionsFilter |
| 拦截器 | logging.interceptor.ts | LoggingInterceptor |
| 中间件 | correlation-id.middleware.ts | CorrelationIdMiddleware |
| Passport 策略 | jwt.strategy.ts | JwtStrategy |
| 枚举 | task-status.enum.ts | TaskStatus |
| 事件 | task-status-changed.event.ts | TaskStatusChangedEvent |
| 单元测试 | projects.service.spec.ts | — |
| 端到端测试 | app.e2e-spec.ts | — |
| 迁移 | 1793093891613-InitialSchema.ts(生成的时间戳) | InitialSchema1793093891613 |
TaskFlow 的其他约定:
- 常量和 token 用
UPPER_SNAKE_CASE:PROJECTS_STORE、CLOCK、TASK_STATUS_CHANGED; - 表名和列名用
snake_case(project_members、due_date),属性用 camelCase:列名要显式指定 (@Column({ name: 'due_date' })),否则 TypeORM 会直接沿用属性名; nest g module|controller|service <名称>按这些规则生成文件,并给控制器和 service 各加一个.spec.ts(J1,CLI)。
后缀是给工具用的
好几处配置按模式挑选文件:
| 配置 | 模式 | 作用 |
|---|---|---|
jest.config.ts | src/.*\.spec\.ts$ | 单元测试文件 |
jest.config.ts(collectCoverageFrom) | *.service.ts、guards.ts、*.filter.ts、*.interceptor.ts | 计入覆盖率的文件 |
test/jest-e2e.json | .e2e-spec.ts$ | 端到端测试文件 |
src/database/data-source.ts | *.entity.{ts,js} | TypeORM CLI(迁移)能看到的实体 |
Swagger 插件(nest-cli.json) | *.dto.ts、*.entity.ts(默认值) | 插件从中推导 OpenAPI schema 的类 |
文件名偏离了后缀,就会悄无声息地落在模式之外,没有任何提示。TaskFlow 里的例子:分页 DTO 命名为 pagination.dto.ts, 这样 Swagger 插件才会读取它,并把 page 和 limit 写进文档(J11);两个 guard 集中在 auth/guards.ts 里,collectCoverageFrom 按文件名(src/**/guards.ts)指定它,让它计入覆盖率 (J10,覆盖率)。同样,一个命名为 project.ts 的实体,migration:generate 根本看不到。