[二期 - 6] 关于Nest的工程规范(CLi、项目结构,monorepo)

1,176 阅读4分钟

文章修正于 2024/02/02 (去掉错别字/把逻辑理更顺)

如何在Main中获取上下文?

有时候我们希望能够在 Main 中获取 AppModule 的上下文,为此你可以这样写

const app = await NestFactory.createApplicationContext(AppModule);
// 如果你想获取 AppModule 上下文 可以直接 使用下面的代码 (注意我们看下面的例子 ConfigService是 在AppModule 已经注册过的)
const configService = app.get(ConfigService) as ConfigService;

const tasksService = app.select(TasksModule).get(TasksService, { strict: true });

关于CLI

我们从一开始就在使用 Nest 提供的 CLI 不管是创建项目骨架还是各个模块都是用的它,实际上你可以单独的使用 nest不一定需要使用他的 CLI,主要还是看 实际的需求,在Newegg 我们更多的并不是依赖Nest的CLI . 从便利程度来看 使用Nest CLI 效率是有一定提升的,特别是对于一般的需求而言

简单的了解一下

我们看看 简单的一些命令 (ng 即视感)

简单的用一下

我们可以使用 npx 或者 npm 或者 yarn全局安装 这个CLI

$ npm install -g @nestjs/cli

# 查看帮助
$ nest --help

# 生成器 
$ nest generate --help

# 下面是一个简单的创建的模板项目的命令 (标准模式)
$ nest new my-nest-project

关于工程结构和设计

关于项目的工程结构,一般而言 nest new 我们仅面对的是 一个 “标准工程”,不过Nest CLI 还提支持其他的工程管理模式,比如 Multiple projectsLib库 他们是用大多数情况下使用 monorepo 来管理的。

下面是一些摘要, 我们展示了不同工程管理模式下的 异同

工程设置标准模式Monorepo模式
Multiple projects分开文件系统结构单个文件系统结构
node_modules & package.json单独的实例可以允许部分依赖互相引用和模块共享
默认的编译器tscwebpack
编译设置单独指定每个项目可以自定义
Config files like , , etc..eslintrc.js.prettierrc单独指定每个项目可以自定义
nest build and commandsnest start目标自动默认为上下文中的(仅)项目目标默认为单存储库中的默认项目
Libraries手动管理,通常通过 npm 打包 内置支持内置支持,包括路径管理和Bundle

看起来有点迷糊我们一会儿实际上来看看就晓得了

CLI的一些基本的指令

# 大部分情况下 都遵循下面的 👇的规范
$ nest commandOrAlias requiredArg [optionalArg] [options]

# 比如这个 表示创建项目 然后去run一下 
$ nest new my-nest-project --dry-run

# 我们还有一些缩写形式 比如
$ nest n my-nest-project -d

# 如果你不知道 某个命令的别名 请使用 
$ nest new --help

WorkSpace

本小节我们来 分析一下 Nestjs 的 CLI生成的项目的骨架 的这些目录和其具体作用,以及 monorepo 模式下下的工程结构和其说明

概述

一般而言在Nest中我们有两种模式来组织代码 ( 标准模式 和 monorepo 模式 ), 所谓的标准模式 就是Nest CLI 直接New 的那种,比较适合独立性较强的项目/service。monorepo 模式问就不详细讲了,Nest CLI 提供了 使用CLI 命令直接生成monorepo 结构的指令很方便,适合相对复杂的项目。

monorepo模式

我们需要按照下面的步骤一步一步 的生成这样的项目结构

# 先生成 标准模式项目( 这样你将会得到一个完整的标准Nest项目骨架 )
$ nest new project
$ cd ./project

# 然后用 CLI 去生存 mono repo 需要的子项目
$ nest generate app my-app

这样就会生成一个 monorepo 的项目它的结构将会是下面的这样样子

├── README.md
├── apps
│   ├── app1
│   │   ├── src
│   │   │   ├── app1.controller.spec.ts
│   │   │   ├── app1.controller.ts
│   │   │   ├── app1.module.ts
│   │   │   ├── app1.service.ts
│   │   │   └── main.ts
│   │   ├── test
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── jest-e2e.json
│   │   └── tsconfig.app.json
│   ├── app2
│   │   ├── src
│   │   │   ├── app2.controller.spec.ts
│   │   │   ├── app2.controller.ts
│   │   │   ├── app2.module.ts
│   │   │   ├── app2.service.ts
│   │   │   └── main.ts
│   │   ├── test
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── jest-e2e.json
│   │   └── tsconfig.app.json
│   └── nestjs-http-server-template
│       ├── src
│       │   ├── app.controller.ts
│       │   ├── app.module.ts
│       │   ├── app.service.ts
│       │   └── main.ts
│       ├── test
│       │   ├── app.e2e-spec.ts
│       │   └── jest-e2e.json
│       └── tsconfig.app.json
├── nest-cli.json
├── package.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock

生成完这样的项目之后 它会 往 nest-cli.json 里面写配置,以供nest cli 再run 的时候去读取它 nest-cli.json

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/nestjs-http-server-template/src",
  "monorepo": true,
  "root": "apps/nestjs-http-server-template",
  "compilerOptions": {
    "webpack": true,
    "tsConfigPath": "apps/nestjs-http-server-template/tsconfig.app.json"
  },
  "projects": {
    "nestjs-http-server-template": {
      "type": "application",
      "root": "apps/nestjs-http-server-template",
      "entryFile": "main",
      "sourceRoot": "apps/nestjs-http-server-template/src",
      "compilerOptions": {
        "tsConfigPath": "apps/nestjs-http-server-template/tsconfig.app.json"
      }
    },
    "app1": {
      "type": "application",
      "root": "apps/app1",
      "entryFile": "main",
      "sourceRoot": "apps/app1/src",
      "compilerOptions": {
        "tsConfigPath": "apps/app1/tsconfig.app.json"
      }
    },
    "app2": {
      "type": "application",
      "root": "apps/app2",
      "entryFile": "main",
      "sourceRoot": "apps/app2/src",
      "compilerOptions": {
        "tsConfigPath": "apps/app2/tsconfig.app.json"
      }
    }
  }
}
# 接下来这样 run 就ok (注意要在工程的root 目录)
$ yarn start app2
$ yarn start app1

接下来我们研究一下 这个 nest-cli.json 的配置到底是干什么用的

研究一下这个 JSON

还是看这份JSON 文件,它分为了两个部分

  1. 项目顶级的全局的,用来控制标准项目/单工程的配置

  2. 子project的单独的配置 ( monorepo 型项目 )

不管是项目级别的还是 全局级别的, 大部分的配置项都大同小异,现在我们首先说说

全局性的配置项目

  • "collection":指向用于生成元件的原理图集合;通常不应更改此值
  • "sourceRoot":指向标准模式结构中单个项目的源代码根,或单存储库模式结构中默认项目的源代码根目录
  • "compilerOptions":带有指定编译器选项的键和指定选项设置的值的映射;请参阅下面的详细信息
  • "generateOptions":带有指定全局生成选项的键和指定选项设置的值的映射;请参阅下面的详细信息
  • "monorepo":(仅限单存储库)对于单存储库模式结构,此值始终为true
  • "root":(仅限 monorepo)指向默认项目的项目根目录

他们在 json 中就是这段配置

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/nestjs-http-server-template/src",
  "monorepo": true,
  "root": "apps/nestjs-http-server-template",
  "compilerOptions": {
    "webpack": true,
    "tsConfigPath": "apps/nestjs-http-server-template/tsconfig.app.json"
  },
  "projects": {}
}

接下来我们进行 更加详细的拆解 编译器配置 注意啊 这里指的就是 compilerOptions

它在JSON中就是这个配置

  ++++
  "compilerOptions": {
    "webpack": true,
    "tsConfigPath": "apps/nestjs-http-server-template/tsconfig.app.json"
  },
  "projects": {
    ++++

它的可选项 和具体作用 见下表

属性类型描述
webpack布尔如果 ,请使用 webpack 编译器。如果不存在,请使用 。在单存储库模式下,默认值为 (使用 webpack),在标准模式下,默认值为 (使用 )。详情见下文。truefalsetsctruefalsetsc
tsConfigPath字符串(仅限单存储库)指向包含将在调用时使用的设置的文件(例如,在生成或启动默认项目时)。tsconfig.jsonnest buildnest startproject
webpackConfigPath字符串指向 webpack 选项文件。如果未指定,Nest 将查找该文件。有关更多详细信息,请参阅下文。webpack.config.js
deleteOutDir布尔如果 ,无论何时调用编译器,它都会首先删除编译输出目录(如 中配置的那样,其中默认值为 )。truetsconfig.json./dist
assets数组允许在编译步骤开始时自动分发非 TypeScript 资产(在模式下的增量编译中不会发生资产分发)。详情见下文。--watch
watchAssets布尔如果 ,则在监视模式下运行,监视所有非 TypeScript 资源。(有关要监视的资产的更精细控制,请参阅下面的资产部分)。true
manualRestart布尔如果为 ,则启用手动重新启动服务器的快捷方式。默认值为 。truersfalse

生成器配置generate

有的时候我们经常会使用 nest 的cli 进行 generate 操作比如 generate controller / service / module, 它也可以通过 nest-cli.json 进行配置

"generateOptions": {
    // "spec": false  全局都禁止 
    "spec": {
      "service": false // 针对某一类 
    }
  },
// 当然这个 generateOptions 既可以 方在全局 也可以放到 子项目中去

指定编译器

一般情况下(标准工程)都是用webpack 进行编译的,如果我们不希望webpack 进行编译,可以给一个false ,这样它就会切到 tsc 进行编译.

"compilerOptions": {
    "webpack": true,
    .....
}

webpack配置

如果你希望自己可以配置 那么可以在根目录下 写具体的配置

~ webpack.config.js
module.exports = function (options) {
  return {
    ...options,
    externals: [],
  };
};
资产

assets 配置 我们可以配置 assets (非ts )文件 构建方式,比如 cv 到 项目root 目录下的doc 下去

 "app2": {
      "type": "application",
      "root": "apps/app2",
      "entryFile": "main",
      "sourceRoot": "apps/app2/src",
      "compilerOptions": {
        "tsConfigPath": "apps/app2/tsconfig.app.json",
        "assets": [
          {  
            "include": "static/**/*", 
             "outDir":"./doc/",
             "watchAssets": true 
          }
        ]
      }
    }

Libraries

刚才我们看的大多数是 monorepo 的app (我的意思是 apps 下都是 类似的结构标准Nest 工程骨架),一般而言就职过规模比较大的公司,对于TOC的项目 会相对更复杂一些 ,往往会使用git submodule + learn 之类的工作来管理,把 通用的 都归类到 某个 共用库去。 而这个共有的库,可以发布到 公司的npm 私有域下,全公司 都可以共享,在大规模协作中,这种方式还是非常的高效的. 比如我所在的Newegg 就是这样的模式. 在Nest 中。默认就提供来了构建这种模式的 cli 程序,十分的方便

$ nest g library my-library
What prefix would you like to use for the library (default: @app)?

# 这样就好了 使用的使用 只需要 @app/xxxx, @app/xxxx
├── libs
│   └── app
│       ├── src
│       │   ├── app.module.ts
│       │   ├── app.service.spec.ts
│       │   ├── app.service.ts
│       │   └── index.ts
│       └── tsconfig.lib.json
├── nest-cli.json
├── package.json
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock

然后它的配置 和大包什么的,都会统一的加到 nest-cli.json中

"project": {
  +++
 "app": {
      "type": "library",
      "root": "libs/app",
      "entryFile": "index",
      "sourceRoot": "libs/app/src",
      "compilerOptions": {
        "tsConfigPath": "libs/app/tsconfig.lib.json"
      }
    }
}

对于tsconfig 的配置 也会一起自动更新掉

~ tsconfig.json
   "paths": {
     "@app/app": [
       "libs/app/src"
     ],
     "@app/app/*": [
       "libs/app/src/*"
     ]
   }
 }
}

比如 我现在 在App2 中使用它 我在 lib下定义的工具

~ libs/app

// 随机生成一段文字
export function randomText() {
 return Math.random().toString(36).substring(2);
}

~ app2 中使用
import { randomText } from '@app/app';
@Controller()
export class App2Controller {
 constructor(private readonly app2Service: App2Service) {}

 @Get()
 getHello(): string {
   return randomText();
 }
}

直接去run app2 就能看得倒效果

yarn start:dev app2

但是更重要的一点的是: 打包的时候会有点奇怪 他生成的目录结构是这样的

    - dist
      apps
          └── app2
              ├── apps
              │   └── app2
              │       └── src
              │           ├── app2.controller.js
              │           ├── app2.controller.js.map
              │           ├── app2.module.js
              │           ├── app2.module.js.map
              │           ├── app2.service.js
              │           ├── app2.service.js.map
              │           ├── main.js
              │           └── main.js.map
              ├── libs
              │   └── app
              │       └── src
              │           ├── index.js
              │           ├── index.js.map
              │           └── service
              │               ├── index.js
              │               └── index.js.map
              └── tsconfig.app.tsbuildinfo