这是《nestjs搭建通用业务框架》系列的第4篇,进入开发具体的功能之前,养成良好的工程目录与代码风格的习惯,目的构建大型复杂项目,提高代码易维护性。
前言 大多数前端同学拿到一个新的任务的时候,或者要做一个新的技术设计的时候,往往无从下手。知乎、掘金上问人可能是一种方案,还可以找一个社交渠道(推特、电报、微博、朋友圈、学校论坛),通过别人的现实例子来进行架构的设计是一个很好切入点。其实,大家可能忽视了以下的渠道:技术框架的官方+示例、公司&团队的历史项目库、找比较厉害的同事取经和发有偿技术咨询的单(程序员各种接单平台)等。
那么,对于nestjs,它的官方提供了很多现成的技术解决方案,所以我们可以借鉴(拿来即用)。
认识CLI 先从官方的CLI开始:
Nest CLI 是一个命令行界面工具,以帮助您初始化、开发和维护 Nest
应用程序。它以多种方式提供帮助,包括搭建项目、以开发模式为其提供服务,以及为生产分发构建和打包应用程序。它体现了最佳实践的架构模式,以构建良好的应用程序。
大多命令行工具可以使用--help
来查看帮助:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 ➜ 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
的形式来进行查看:
特别说明:
名称
缩写
描述
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 这里有作者的回复。
1 2 3 4 5 6 7 8 9 10 11 12 13 - src - core - common - middleware - interceptors - guards - user - interceptors (scoped interceptors) - user.controller.ts - user.model.ts - store - store.controller.ts - store.model.ts
参考项目 第一个参考项目
技术栈:Nest + sequelize-typescript + JWT + Jest + Swagger
项目地址:kentloog/nestjs-sequelize-typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 . ├── 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 . ├── 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 . ├── 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
下面的项目结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 +-- 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.+ | + | + | + | + | + | + | +
特点:
项目文档及相关的资源在根目录,包括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等工具来整理代码格式、检查代码风格问题。