这是《nestjs搭建通用业务框架》系列的第4篇,进入开发具体的功能之前,养成良好的工程目录与代码风格的习惯,目的构建大型复杂项目,提高代码易维护性。
大多数前端同学拿到一个新的任务的时候,或者要做一个新的技术设计的时候,往往无从下手。知乎、掘金上问人可能是一种方案,还可以找一个社交渠道(推特、电报、微博、朋友圈、学校论坛),通过别人的现实例子来进行架构的设计是一个很好切入点。其实,大家可能忽视了以下的渠道:技术框架的官方+示例、公司&团队的历史项目库、找比较厉害的同事取经和发有偿技术咨询的单(程序员各种接单平台)等。
那么,对于nestjs,它的官方提供了很多现成的技术解决方案,所以我们可以借鉴(拿来即用)。
先从官方的CLI开始:
Nest CLI是一个命令行界面工具,以帮助您初始化、开发和维护
Nest
应用程序。它以多种方式提供帮助,包括搭建项目、以开发模式为其提供服务,以及为生产分发构建和打包应用程序。它体现了最佳实践的架构模式,以构建良好的应用程序。
大多命令行工具可以使用--help
来查看帮助:
➜ nest --help Usage: nest <command> [options] Options: -v, --version Output the current version. -h, --help Output usage information. Commands: new|n [options] [name] Generate Nest application. build [options] [app] Build Nest application. start [options] [app] Run Nest application. info|i Display Nest project details. update|u [options] Update Nest dependencies. add [options] <library> Adds support for an external library to your project. generate|g [options] <schematic> [name] [path] Generate a Nest element. Available schematics: ┌───────────────┬─────────────┬──────────────────────────────────────────────┐ │ name │ alias │ description │ │ application │ application │ Generate a new application workspace │ │ class │ cl │ Generate a new class │ │ configuration │ config │ Generate a CLI configuration file │ │ controller │ co │ Generate a controller declaration │ │ decorator │ d │ Generate a custom decorator │ │ filter │ f │ Generate a filter declaration │ │ gateway │ ga │ Generate a gateway declaration │ │ guard │ gu │ Generate a guard declaration │ │ interceptor │ in │ Generate an interceptor declaration │ │ interface │ interface │ Generate an interface │ │ middleware │ mi │ Generate a middleware declaration │ │ module │ mo │ Generate a module declaration │ │ pipe │ pi │ Generate a pipe declaration │ │ provider │ pr │ Generate a provider declaration │ │ resolver │ r │ Generate a GraphQL resolver declaration │ │ service │ s │ Generate a service declaration │ │ library │ lib │ Generate a new library within a monorepo │ │ sub-app │ app │ Generate a new application within a monorepo │ │ resource │ res │ Generate a new CRUD resource │ └───────────────┴─────────────┴──────────────────────────────────────────────┘
然后,如果想知道其中某一个子命令的用法,可以使用nest <command> --help
的形式来进行查看:
1 2 | # 例如: ➜ nest generate --help |
特别说明:
名称 | 缩写 | 描述 |
---|---|---|
application | application | 生成一个新的应用工作区 |
class | cl | 生成一个新的class |
configuration | config | 生成 CLI 配置文件 |
controller | co | 生成一个控制器声明 |
decorator | d | 生成一个自定义的装饰者 |
filter | f | 生成一个过滤器声明 |
gateway | ga | 生成网关 |
guard | gu | 生成守卫 |
interceptor | in | 生成拦截器 |
interface | interface | 生成接口声明 |
middleware | mi | 生成中间件声明 |
module | mo | 生成一个模块声明 |
pipe | pi | 生成管道声明 |
provider | pr | 生成提供者声明 |
resolver | r | 生成GraphQL resolver声明 |
service | s | 生成服务 |
library | lib | 生成一个monorepo库 |
sub-app | App | 生成一个monorepo的应用 |
resource | Res | 生成一个新的CURD资源 |
我们最开始使用了一个new
命令,后面最常用的即是generator
或g
(简写)命令,可以对照着上表进行熟悉。
为了去理解Python的语言设计之美,其实更要理解这样的一句话“约定大于配置”,好的工程化目录(约定)能够很好的提升项目的可维护性。
在官方的issues中,我们可以找到一些提示:Best scalable project structure #2249 这里有作者的回复。
- src - core - common - middleware - interceptors - guards - user - interceptors (scoped interceptors) - user.controller.ts - user.model.ts - store - store.controller.ts - store.model.ts
可以使用monorepo的方法——在一个repo中创建两个项目,并在它们之间共享共同的东西,如库/包。
没有模块目录,按照功能进行划分。
把通用/核心的东西归为单独的目录:common,比如:拦截器/守卫/管道
第一个参考项目
技术栈:Nest + sequelize-typescript + JWT + Jest + Swagger
项目地址:kentloog/nestjs-sequelize-typescript
. ├── README.md ├── assets │ └── logo.png ├── config │ ├── config.development.ts │ └── config.production.ts ├── config.ts ├── db │ ├── config.ts │ ├── migrations │ │ └── 20190128160000-create-table-user.js │ └── seeders-dev │ └── 20190129093300-test-data-users.js ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── src │ ├── app.module.ts │ ├── database │ │ ├── database.module.ts │ │ └── database.providers.ts │ ├── main.ts │ ├── posts │ │ ├── dto │ │ │ ├── create-post.dto.ts │ │ │ ├── post.dto.ts │ │ │ └── update-post.dto.ts │ │ ├── post.entity.ts │ │ ├── posts.controller.ts │ │ ├── posts.module.ts │ │ ├── posts.providers.ts │ │ └── posts.service.ts │ ├── shared │ │ ├── config │ │ │ └── config.service.ts │ │ ├── enum │ │ │ └── gender.ts │ │ └── shared.module.ts │ ├── swagger.ts │ └── users │ ├── auth │ │ ├── jwt-payload.model.ts │ │ └── jwt-strategy.ts │ ├── dto │ │ ├── create-user.dto.ts │ │ ├── update-user.dto.ts │ │ ├── user-login-request.dto.ts │ │ ├── user-login-response.dto.ts │ │ └── user.dto.ts │ ├── user.entity.ts │ ├── users.controller.ts │ ├── users.module.ts │ ├── users.providers.ts │ └── users.service.ts ├── test │ ├── app.e2e-spec.ts │ ├── jest-e2e.json │ └── test-data.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json
特点:
项目文档及相关的资源在根目录
数据库及项目配置会放在根目录(细节:数据库升级文件)
src
中会对功能进行划分建不同的文件夹users
、posts
单个功能文件夹中,会包括一个完整CURD的相关文件(dto/controller/module/providers/service)
抽离公共配置到shared
文件夹
第二个参考项目
技术栈:具有AWS Lambda,DynamoDB,DynamoDB Streams的完全无服务器生产应用程序
项目地址:International-Slackline-Association/Rankings-Backend
. ├── LICENSE ├── README.md ├── docs │ ├── AWS_Architecture.png │ ├── Development\ Notes.md │ └── GourceOutput.png ├── jest.config.js ├── package-lock.json ├── package.json ├── serverless │ ├── environment.yml │ └── secrets.example.yml ├── serverless.yml ├── src │ ├── api │ │ ├── admin │ │ │ ├── api.module.ts │ │ │ ├── athlete │ │ │ ├── contest │ │ │ ├── database.module.ts │ │ │ ├── index.ts │ │ │ ├── results │ │ │ └── submit │ │ └── webapp │ │ ├── api.module.ts │ │ ├── athlete │ │ ├── contest │ │ ├── country │ │ ├── database.module.ts │ │ ├── index.ts │ │ ├── nestjsTest.controller.ts │ │ └── rankings │ ├── core │ │ ├── athlete │ │ │ ├── athlete.service.ts │ │ │ ├── entity │ │ │ ├── interfaces │ │ │ └── rankings.service.ts │ │ ├── aws │ │ │ ├── aws.module.ts │ │ │ ├── aws.services.interface.ts │ │ │ └── aws.services.ts │ │ ├── category │ │ │ └── categories.service.ts │ │ ├── contest │ │ │ ├── contest.service.ts │ │ │ ├── entity │ │ │ └── points-calculator.service.ts │ │ └── database │ │ ├── database.module.ts │ │ ├── database.service.ts │ │ ├── dynamodb │ │ ├── redis │ │ └── test │ ├── cron-job │ │ ├── cron-job.module.ts │ │ ├── cron-job.service.ts │ │ ├── cron-job.spec.ts │ │ ├── database.module.ts │ │ └── index.ts │ ├── dynamodb-streams │ │ ├── athlete │ │ │ ├── athlete-contest-record.service.ts │ │ │ ├── athlete-details-record.service.ts │ │ │ └── athlete-records.module.ts │ │ ├── contest │ │ │ ├── contest-record.service.ts │ │ │ └── contest-records.module.ts │ │ ├── database.module.ts │ │ ├── dynamodb-streams.module.ts │ │ ├── dynamodb-streams.service.ts │ │ ├── index.ts │ │ ├── test │ │ │ ├── contest-modifications.spec.ts │ │ │ └── lambda-trigger.ts │ │ └── utils.ts │ ├── image-resizer │ │ ├── S3Events.module.ts │ │ ├── S3Events.service.ts │ │ ├── database.module.ts │ │ ├── index.ts │ │ ├── test │ │ │ ├── lambda-trigger.ts │ │ │ └── s3-image-put.spec.ts │ │ └── thumbnail-creator │ │ ├── imagemagick.ts │ │ ├── s3.service.ts │ │ ├── thumbnail-creator.module.ts │ │ └── thumbnail-creator.service.ts │ └── shared │ ├── constants.ts │ ├── decorators │ │ └── roles.decorator.ts │ ├── enums │ │ ├── contestType-utility.ts │ │ ├── discipline-utility.ts │ │ ├── enums-utility.ts │ │ └── index.ts │ ├── env_variables.ts │ ├── exceptions │ │ ├── api.error.ts │ │ └── api.exceptions.ts │ ├── extensions.ts │ ├── filters │ │ └── exception.filter.ts │ ├── generators │ │ └── id.generator.ts │ ├── guards │ │ └── roles.guard.ts │ ├── index.ts │ ├── logger.ts │ ├── pipes │ │ └── JoiValidation.pipe.ts │ ├── types │ │ ├── express.d.ts │ │ ├── extensions.d.ts │ │ └── shared.d.ts │ └── utils.ts ├── test │ ├── jest-e2e.json │ └── test-setup.ts ├── tsconfig.json ├── tslint.json └── webpack ├── webpack.config.Dev.js ├── webpack.config.Prod.js ├── webpack.config.Test.js └── webpack.config.base.js
特点:
根目录中存放webpack、微服务配置 + 项目文档
src
中会对功能进行划分建不同的文件夹: api
、core
、dynamodb-stream
、image-resizer
在核心模块中,按照功能模块进划分,与之相关的entity、service放置在同一文件夹中
抽离公共配置到shared
文件夹:常量、自定义的装饰器、统一错误处理、过滤器、生成器、守卫、日志服务
第三个参考项目:
技术栈:使用 NestJS 的 Blog/CMS, RESTful API 服务端应用
项目地址:surmon-china/nodepressTemplate
. ├── API_DOC.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── classified ├── cspell.json ├── dbbackup ├── logo.png ├── logo.psd ├── nest-cli.json ├── package.json ├── scripts │ ├── README.md │ ├── dbbackup.sh │ ├── dbrecover.sh │ └── deploy.sh ├── src │ ├── app.config.ts │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.environment.ts │ ├── app.module.ts │ ├── constants │ │ ├── cache.constant.ts │ │ ├── meta.constant.ts │ │ ├── system.constant.ts │ │ └── text.constant.ts │ ├── decorators │ │ ├── cache.decorator.ts │ │ ├── http.decorator.ts │ │ └── query-params.decorator.ts │ ├── errors │ │ ├── bad-request.error.ts │ │ ├── custom.error.ts │ │ ├── forbidden.error.ts │ │ ├── unauthorized.error.ts │ │ └── validation.error.ts │ ├── filters │ │ └── error.filter.ts │ ├── guards │ │ ├── auth.guard.ts │ │ └── humanized-auth.guard.ts │ ├── interceptors │ │ ├── cache.interceptor.ts │ │ ├── error.interceptor.ts │ │ ├── logging.interceptor.ts │ │ └── transform.interceptor.ts │ ├── interfaces │ │ ├── http.interface.ts │ │ ├── mongoose.interface.ts │ │ └── state.interface.ts │ ├── main.ts │ ├── middlewares │ │ ├── cors.middleware.ts │ │ └── origin.middleware.ts │ ├── models │ │ └── extend.model.ts │ ├── modules │ │ ├── announcement │ │ │ ├── announcement.controller.ts │ │ │ ├── announcement.model.ts │ │ │ ├── announcement.module.ts │ │ │ └── announcement.service.ts │ │ ├── article │ │ │ ├── article.controller.ts │ │ │ ├── article.model.ts │ │ │ ├── article.module.ts │ │ │ └── article.service.ts │ │ ├── auth │ │ │ ├── auth.controller.ts │ │ │ ├── auth.interface.ts │ │ │ ├── auth.model.ts │ │ │ ├── auth.module.ts │ │ │ ├── auth.service.ts │ │ │ └── jwt.strategy.ts │ │ ├── category │ │ │ ├── category.controller.ts │ │ │ ├── category.model.ts │ │ │ ├── category.module.ts │ │ │ └── category.service.ts │ │ ├── comment │ │ │ ├── comment.controller.ts │ │ │ ├── comment.model.ts │ │ │ ├── comment.module.ts │ │ │ └── comment.service.ts │ │ ├── expansion │ │ │ ├── expansion.controller.ts │ │ │ ├── expansion.module.ts │ │ │ ├── expansion.service.dbbackup.ts │ │ │ └── expansion.service.statistic.ts │ │ ├── like │ │ │ ├── like.controller.ts │ │ │ ├── like.module.ts │ │ │ └── like.service.ts │ │ ├── option │ │ │ ├── option.controller.ts │ │ │ ├── option.model.ts │ │ │ ├── option.module.ts │ │ │ └── option.service.ts │ │ ├── syndication │ │ │ ├── syndication.controller.ts │ │ │ ├── syndication.module.ts │ │ │ └── syndication.service.ts │ │ └── tag │ │ ├── tag.controller.ts │ │ ├── tag.model.ts │ │ ├── tag.module.ts │ │ └── tag.service.ts │ ├── pipes │ │ └── validation.pipe.ts │ ├── processors │ │ ├── cache │ │ │ ├── cache.config.service.ts │ │ │ ├── cache.module.ts │ │ │ └── cache.service.ts │ │ ├── database │ │ │ ├── database.module.ts │ │ │ └── database.provider.ts │ │ └── helper │ │ ├── helper.module.ts │ │ ├── helper.service.akismet.ts │ │ ├── helper.service.cs.ts │ │ ├── helper.service.email.ts │ │ ├── helper.service.google.ts │ │ ├── helper.service.ip.ts │ │ └── helper.service.seo.ts │ └── transformers │ ├── codec.transformer.ts │ ├── error.transformer.ts │ ├── model.transformer.ts │ ├── mongoose.transformer.ts │ └── urlmap.transformer.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock
特点:
项目文档及相关的资源在根目录
src
中modules
会对功能进行划分建不同的文件夹
单个功能文件夹中,会包括一个完整CURD的相关文件(model/controller/module/service)
把公共的代码(按照nestjs逻辑分层)拆成单独的文件夹guards
、filters
、decorators
、interceptors
、interfaces
、errors
项目:CatsMiaow/node-nestjs-structure
下面的项目结构:
+-- bin // Custom tasks +-- dist // Source build +-- public // Static Files +-- src | +-- config // Environment Configuration | +-- entity // TypeORM Entities generated by `typeorm-model-generator` module | +-- auth // Authentication | +-- common // Global Nest Module | | +-- constants // Constant value and Enum | | +-- controllers // Nest Controllers | | +-- decorators // Nest Decorators | | +-- dto // DTO (Data Transfer Object) Schema, Validation | | +-- filters // Nest Filters | | +-- guards // Nest Guards | | +-- interceptors // Nest Interceptors | | +-- interfaces // TypeScript Interfaces | | +-- middleware // Nest Middleware | | +-- pipes // Nest Pipes | | +-- providers // Nest Providers | | +-- * // models, repositories, services... | +-- shared // Shared Nest Modules | +-- gql // GraphQL Structure Sample | +-- * // Other Nest Modules, non-global, same as common structure above +-- test // Jest testing +-- typings // Modules and global type definitions |
如果是功能模块:
1 2 3 4 5 6 7 8 9 10 | // Module structure // Add folders according to module scale. If it's small, you don't need to add folders. +-- src/greeter | +-- * // folders | +-- greeter.constant.ts | +-- greeter.controller.ts | +-- greeter.service.ts | +-- greeter.module.ts | +-- greeter.*.ts | +-- index.ts |
特点:
项目文档及相关的资源在根目录,包括typings
、test
、bin
src
中会对功能进行划分建不同的文件夹
抽离公共代码到common
文件夹,配置文件放在config
文件夹,实体类放置在entity
中
鉴权相关的逻辑放在auth
把同类的guards
、filters
、decorators
、interceptors
、interfaces
、errors
存放在common
文件夹中
我们对Angular风格指南进行了摘抄,如下:
参考:Angular风格指南
坚持每个文件只定义一样东西(例如服务或组件)
考虑把文件大小限制在 400 行代码以内
坚持定义简单函数
考虑限制在 75 行之内
坚持所有符号使用一致的命名规则
坚持遵循同一个模式来描述符号的特性和类型
坚持 在描述性名字中,用横杠来分隔单词。
坚持使用点来分隔描述性名字和类型。
坚持遵循先描述组件特性,再描述它的类型的模式,对所有组件使用一致的类型命名规则。推荐的模式为 feature.type.ts
。
坚持使用惯用的后缀来描述类型,包括 *.service
、*.component
、*.pipe
、.module
、.directive
。 必要时可以创建更多类型名,但必须注意,不要创建太多。
坚持为所有东西使用一致的命名约定,以它们所代表的东西命名。
坚持使用大写驼峰命名法来命名类
坚持匹配符号名与它所在的文件名
坚持在符号名后面追加约定的类型后缀(例如 Component
、Directive
、Module
、Pipe
、Service
)。
坚持在文件名后面追加约定的类型后缀(例如 .component.ts
、.directive.ts
、.module.ts
、.pipe.ts
、.service.ts
)
坚持使用中线命名法(dashed-case)或叫烤串命名法(kebab-case)来命名组件的元素选择器。
坚持使用一致的规则命名服务,以它们的特性来命名
坚持为服务的类名加上 Service
后缀。 例如,获取数据或英雄列表的服务应该命名为 DataService
或 HeroService
坚持为所有管道使用一致的命名约定,用它们的特性来命名。 管道类名应该使用 UpperCamelCase(类名的通用约定),而相应的 name
字符串应该使用 lowerCamelCase。 name
字符串中不应该使用中线(“中线格式”或“烤串格式”)。例如:
ellipsis.pipe.ts
1 2 | @Pipe({ name: 'ellipsis' }) export class EllipsisPipe implements PipeTransform { } |
和
init-caps.pipe.ts
1 2 | @Pipe({ name: 'initCaps' }) export class InitCapsPipe implements PipeTransform { } |
坚持在模块中只包含模块间的依赖关系,所有其它逻辑都应该放到服务中
坚持把可复用的逻辑放到服务中,保持组件简单,聚焦于它们预期目的
坚持在同一个注入器内,把服务当做单例使用,用它们来共享数据和功能
坚持创建封装在上下文中的单一职责的服务
坚持当服务成长到超出单一用途时,创建一个新服务
坚持把数据操作和与数据交互的逻辑重构到服务里。
坚持把应用的引导程序和平台相关的逻辑放到名为 main.ts
的文件里
坚持在引导逻辑中包含错误处理代码
避免把应用逻辑放在 main.ts
中,而应放在组件或服务里
单元测试:
坚持测试规格文件名与被测试组件文件名相同
坚持测试规格文件名添加 .spec
后缀
端到端的测试:
坚持端到端测试规格文件和它们所测试的特性同名,添加 .e2e-spec
后缀,或者放在特定的文件夹中。
定位:
坚持直观、简单和快速地定位代码。
识别:
坚持命名文件到这个程度:看到名字立刻知道它包含了什么,代表了什么。
坚持文件名要具有说明性,确保文件中只包含一个组件。
避免创建包含多个组件、服务或者混合体的文件。
扁平
坚持尽可能保持扁平的目录结构。
考虑当同一目录下达到 7 个或更多个文件时创建子目录。
考虑配置 IDE,以隐藏无关的文件,例如生成出来的 .js
文件和 .js.map
文件等。
T-DRY
坚持 DRY(Don’t Repeat Yourself,不重复自己)。
避免过度 DRY,以致牺牲了阅读性
代码结构
坚持从零开始,但要考虑应用程序接下来的路往哪儿走
坚持有一个近期实施方案和一个长期的愿景
坚持把所有源代码都放到名为 src
的目录里
坚持如果组件具有多个伴生文件 (.ts
、.html
、.css
和 .spec
),就为它创建一个文件夹
ESLint
坚持使用VSCode等IDE、配合ESLint + Prettier等工具来整理代码格式、检查代码风格问题。