Skip to content

仓库结构 ​

计划给出了一个目标目录结构和几条命名约定。本页把它们和 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.md

TaskFlow 的实际结构 ​

项目在 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、decoratorscommon/:filters、interceptors、middleware、decorators、dto内置管道(ParseUUIDPipe、ParseEnumPipe)就够用;追踪 ID 中间件和分页 DTO 是共用的
database/migrations/另有 database.module.ts、postgres-options.ts、data-source.ts、seed.tsTypeORM 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.tsJ11 的 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)。

模块文件导出被谁导入
CommonModulecommon/common.module.tsCLOCK(@Global() 模块)全局可见
DatabaseModuledatabase/database.module.ts—(配置 TypeORM)AppModule
UsersModuleusers/users.module.tsUsersServiceAuthModule
AuthModuleauth/auth.module.ts—AppModule
ProjectsModuleprojects/projects.module.tsProjectsService(不导出 store)TasksModule
TasksModuletasks/tasks.module.ts—AppModule
NotificationsModulenotifications/notifications.module.ts—AppModule;定义了 REDIS_URL 时导入 NotificationsQueueModule
HealthModulehealth/health.controller.ts—AppModule
DemoModuledemo/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.tsProjectsModule
控制器projects.controller.tsProjectsController
Serviceprojects.service.tsProjectsService
实体project.entity.ts、project-member.entity.tsProject、ProjectMember
DTOcreate-project.dto.ts(计划、nest g)或 project.dto.ts(TaskFlow)CreateProjectDto
过滤器all-exceptions.filter.tsAllExceptionsFilter
拦截器logging.interceptor.tsLoggingInterceptor
中间件correlation-id.middleware.tsCorrelationIdMiddleware
Passport 策略jwt.strategy.tsJwtStrategy
枚举task-status.enum.tsTaskStatus
事件task-status-changed.event.tsTaskStatusChangedEvent
单元测试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.tssrc/.*\.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 根本看不到。