Skip to content

类库

很多应用都需要解决相同的一类通用问题,或者在多个场景中复用某个模块化组件。Nest 提供了几种不同层级的手段来支持这类需求,每种方式都面向不同的架构与组织目标。

Nest 的模块非常适合在单个应用内部提供执行上下文,从而实现组件共享。模块也可以通过 npm 打包为可复用的类库,并安装到不同项目中。这种方式非常适合发布可配置、可复用的类库,供彼此联系较弱甚至完全独立的组织使用(例如第三方库的分发与安装)。

而对于组织边界更紧密的团队(例如公司内部、同一项目组内部)来说,通常会希望有一种更轻量的共享组件方式。Monorepo 就是在这种背景下流行起来的,而在 monorepo 内部,类库正是实现轻量共享代码的方式之一。在 Nest monorepo 中,使用类库可以非常方便地组装共享组件的应用。事实上,这种结构也鼓励你将单体应用拆分为更聚焦的模块化组件,并围绕组件组合来组织开发流程。

Nest 类库

Nest 类库本质上也是一个 Nest 项目,只不过它与应用的区别在于:它不能独立运行。类库必须被导入到某个应用中,其代码才会真正执行。本节介绍的内置类库支持仅适用于 monorepo;在标准模式项目中,你可以通过 npm 包来实现类似效果。

例如,一个组织可能会开发一个 AuthModule,用于统一处理认证逻辑,并实现适用于所有内部应用的公司级安全策略。与其为每个应用单独复制这套模块,或者将其打包成 npm 包再让每个项目单独安装,不如直接在 monorepo 中把它定义成一个类库。采用这种组织方式后,所有依赖该类库的应用都可以始终看到最新提交的 AuthModule。这对于协调组件开发与组装、以及简化端到端测试都很有帮助。

创建类库

任何适合复用的功能,都可以考虑作为类库管理。至于什么应该抽成类库、什么应该保留在应用内部,这是一个架构设计问题。创建类库并不只是简单地把已有应用中的代码复制过去。类库一旦被抽离,就要求它的代码能够与具体应用解耦。这通常意味着你需要在前期投入更多时间,也会迫使你提前做出一些在强耦合代码里可能暂时不需要面对的设计决策。但一旦设计得当,这些额外投入会在多个应用复用和更快组装系统时得到回报。

要开始创建类库,请执行:

bash
$ nest g library my-library

执行命令后,library schematic 会提示你为这个类库指定一个前缀(也就是别名):

bash
What prefix would you like to use for the library (default: @app)?

这会在你的 workspace 中创建一个名为 my-library 的新项目。 与应用项目一样,类库项目也是通过 schematic 生成到一个命名目录中的。类库统一位于 monorepo 根目录下的 libs 文件夹中;第一次创建类库时,Nest 会自动生成 libs 目录。

类库生成出的文件与应用略有不同。执行上述命令后,libs 目录内容大致如下:

plaintext
libs
  my-library
    src
      index.ts
      my-library.module.ts
      my-library.service.ts
    tsconfig.lib.json

此时,nest-cli.json 文件中的 "projects" 节点下也会新增一项:

javascript
{
  "my-library": {
    "type": "library",
    "root": "libs/my-library",
    "entryFile": "index",
    "sourceRoot": "libs/my-library/src",
    "compilerOptions": {
      "tsConfigPath": "libs/my-library/tsconfig.lib.json"
    }
  }
}

nest-cli.json 中,类库与应用的元数据有两个关键区别:

  • "type" 会被设置为 "library",而不是 "application"
  • "entryFile" 会被设置为 "index",而不是 "main"

这些差异会让构建过程按类库的方式处理项目。例如,类库会通过 index.js 文件对外导出其功能。

与应用项目一样,类库也有自己的 tsconfig.lib.json 文件,它会继承 monorepo 根级别的 tsconfig.json。如有需要,你可以修改该文件,为类库设置专属的编译选项。

你可以通过下面的 CLI 命令构建类库:

bash
$ nest build my-library

使用类库

在自动生成的配置文件都准备就绪之后,使用类库就会非常直接。假设我们想在 my-project 应用中使用 my-library 类库里的 MyLibraryService,该怎么做?

首先要注意,使用类库模块与使用任何其他 Nest 模块本质上没有区别。Monorepo 帮你做的事情,是通过路径管理让“导入类库”和“生成构建产物”变得透明。要使用 MyLibraryService,我们需要先导入它所在的声明模块。因此,可以像下面这样修改 my-project/src/app.module.ts,导入 MyLibraryModule

typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyLibraryModule } from '@app/my-library';

@Module({
  imports: [MyLibraryModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

请注意,上面的 ES module import 语句使用了 @app 这样的路径别名,这正是此前执行 nest g library 时提供的 prefix。在底层,Nest 是通过 tsconfig 的 path mapping 来实现这一点的。新增类库时,Nest 会自动更新全局(monorepo 级别)tsconfig.json 文件中的 "paths"

javascript
"paths": {
  "@app/my-library": [
    "libs/my-library/src"
  ],
  "@app/my-library/*": [
    "libs/my-library/src/*"
  ]
}

简而言之,monorepo 与类库这两项特性结合起来,使得在应用中引入类库模块这件事既简单又直观。

同样的机制也使得“构建并部署由类库组成的应用”变得容易。一旦你导入了 MyLibraryModule,执行 nest build 时,模块解析和打包都会自动处理,最终生成的应用构建产物会同时包含所有类库依赖。

Monorepo 模式默认使用的编译器是 webpack,因此最终生成的发布文件通常是一个单文件包,其中包含所有转译后的 JavaScript 代码。你也可以像工作空间章节中介绍的那样切换为 tsc

基于 NestJS 官方文档翻译