手记

nestjs搭建通用业务框架(4):工程目录与代码规范

2021-03-18 11:05:097000浏览

Brian

5实战 · 15手记 · 8推荐

这是《nestjs搭建通用业务框架》系列的第4篇,进入开发具体的功能之前,养成良好的工程目录与代码风格的习惯,目的构建大型复杂项目,提高代码易维护性。


前言

大多数前端同学拿到一个新的任务的时候,或者要做一个新的技术设计的时候,往往无从下手。知乎、掘金上问人可能是一种方案,还可以找一个社交渠道(推特、电报、微博、朋友圈、学校论坛),通过别人的现实例子来进行架构的设计是一个很好切入点。其实,大家可能忽视了以下的渠道:技术框架的官方+示例、公司&团队的历史项目库、找比较厉害的同事取经和发有偿技术咨询的单(程序员各种接单平台)等。

那么,对于nestjs,它的官方提供了很多现成的技术解决方案,所以我们可以借鉴(拿来即用)。


认识CLI

先从官方的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

特别说明:

名称缩写描述
applicationapplication生成一个新的应用工作区
classcl生成一个新的class
configurationconfig生成 CLI 配置文件
controllerco生成一个控制器声明
decoratord生成一个自定义的装饰者
filterf生成一个过滤器声明
gatewayga生成网关
guardgu生成守卫
interceptorin生成拦截器
interfaceinterface生成接口声明
middlewaremi生成中间件声明
modulemo生成一个模块声明
pipepi生成管道声明
providerpr生成提供者声明
resolverr生成GraphQL resolver声明
services生成服务
librarylib生成一个monorepo库
sub-appApp生成一个monorepo的应用
resourceRes生成一个新的CURD资源

我们最开始使用了一个new命令,后面最常用的即是generatorg(简写)命令,可以对照着上表进行熟悉。


合理的工程目录

为了去理解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中会对功能进行划分建不同的文件夹usersposts

  • 单个功能文件夹中,会包括一个完整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中会对功能进行划分建不同的文件夹: apicoredynamodb-streamimage-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

特点:

  • 项目文档及相关的资源在根目录

  • srcmodules会对功能进行划分建不同的文件夹

  • 单个功能文件夹中,会包括一个完整CURD的相关文件(model/controller/module/service)

  • 把公共的代码(按照nestjs逻辑分层)拆成单独的文件夹guardsfiltersdecoratorsinterceptorsinterfaceserrors




最佳实践

项目: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

特点:

  • 项目文档及相关的资源在根目录,包括typingstestbin

  • src中会对功能进行划分建不同的文件夹

  • 抽离公共代码到common文件夹,配置文件放在config文件夹,实体类放置在entity

  • 鉴权相关的逻辑放在auth

  • 把同类的guardsfiltersdecoratorsinterceptorsinterfaceserrors存放在common文件夹中


总则

坚持每个文件只定义一样东西(例如服务或组件)

考虑把文件大小限制在 400 行代码以内

坚持定义简单函数

考虑限制在 75 行之内

命名

坚持所有符号使用一致的命名规则

坚持遵循同一个模式来描述符号的特性和类型

使用点和横杠来分隔文件名

坚持 在描述性名字中,用横杠来分隔单词。

坚持使用点来分隔描述性名字和类型。

坚持遵循先描述组件特性,再描述它的类型的模式,对所有组件使用一致的类型命名规则。推荐的模式为 feature.type.ts

坚持使用惯用的后缀来描述类型,包括 *.service*.component*.pipe.module.directive。 必要时可以创建更多类型名,但必须注意,不要创建太多。

符号名与文件名

坚持为所有东西使用一致的命名约定,以它们所代表的东西命名。

坚持使用大写驼峰命名法来命名类

坚持匹配符号名与它所在的文件名

坚持在符号名后面追加约定的类型后缀(例如 ComponentDirectiveModulePipeService)。

坚持在文件名后面追加约定的类型后缀(例如 .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等工具来整理代码格式、检查代码风格问题。


3人推荐
随时随地看视频
慕课网APP