Overview

模块

学习如何在 NestJS 中使用模块来组织应用程序结构,包括功能模块、共享模块、模块重新导出、依赖注入和动态模块的概念。

模块是一个用 @Module() 装饰器注释的类。该装饰器提供了 Nest 用来高效组织和管理应用程序结构的元数据。

每个 Nest 应用程序至少有一个模块,即根模块,它作为 Nest 构建应用程序图的起点。该图是 Nest 用来解析模块和提供者之间关系和依赖项的内部结构。虽然小型应用程序可能只有一个根模块,但通常情况并非如此。强烈建议使用模块作为组织组件的有效方式。对于大多数应用程序,您可能会有多个模块,每个模块封装一组密切相关的功能

@Module() 装饰器接受一个对象,其属性描述模块:

属性描述
providers将由 Nest 注入器实例化并且至少可以在此模块中共享的提供者
controllers在此模块中定义的必须实例化的控制器集合
imports导入的模块列表,这些模块导出此模块所需的提供者
exports此模块提供的 providers 子集,应该在导入此模块的其他模块中可用。您可以使用提供者本身或仅使用其令牌(provide 值)

模块默认封装提供者,这意味着您只能注入属于当前模块或从其他导入模块显式导出的提供者。模块导出的提供者本质上充当模块的公共接口或 API。

功能模块

在我们的示例中,CatsControllerCatsService 密切相关并服务于同一个应用程序域。将它们分组到功能模块中是有意义的。功能模块组织与特定功能相关的代码,有助于保持清晰的边界和更好的组织。随着应用程序或团队的增长,这一点尤为重要,并且符合 SOLID 原则。

接下来,我们将创建 CatsModule 来演示如何将控制器和服务分组。

@@filename(cats/cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

info 提示 要使用 CLI 创建模块,只需执行 $ nest g module cats 命令。

上面,我们在 cats.module.ts 文件中定义了 CatsModule,并将与此模块相关的所有内容移动到 cats 目录中。我们需要做的最后一件事是将此模块导入到根模块(在 app.module.ts 文件中定义的 AppModule)中。

@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}

现在我们的目录结构如下所示:

src/
├── cats/
│   ├── dto/
│   │   └── create-cat.dto.ts
│   ├── interfaces/
│   │   └── cat.interface.ts
│   ├── cats.controller.ts
│   ├── cats.module.ts
│   └── cats.service.ts
├── app.module.ts
└── main.ts

共享模块

在 Nest 中,模块默认是单例的,因此您可以轻松地在多个模块之间共享任何提供者的同一实例。

每个模块都自动是一个共享模块。一旦创建,它就可以被任何模块重用。让我们想象一下,我们想要在几个其他模块之间共享 CatsService 的实例。为了做到这一点,我们首先需要通过将 CatsService 提供者添加到模块的 exports 数组中来导出它,如下所示:

@@filename(cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

现在任何导入 CatsModule 的模块都可以访问 CatsService,并将与所有其他导入它的模块共享同一实例。

如果我们在每个需要它的模块中直接注册 CatsService,它确实会工作,但会导致每个模块获得自己独立的 CatsService 实例。这可能导致内存使用增加,因为创建了同一服务的多个实例,如果服务维护任何内部状态,还可能导致意外行为,如状态不一致。

通过将 CatsService 封装在模块(如 CatsModule)内并导出它,我们确保在所有导入 CatsModule 的模块中重用 CatsService 的同一实例。这不仅减少了内存消耗,还导致更可预测的行为,因为所有模块共享同一实例,使管理共享状态或资源变得更容易。这是像 NestJS 这样的框架中模块化和依赖注入的关键优势之一——允许服务在整个应用程序中高效共享。

模块重新导出

如上所示,模块可以导出其内部提供者。此外,它们还可以重新导出它们导入的模块。在下面的示例中,CommonModule 既被导入到 CoreModule CoreModule 导出,使其可供导入此模块的其他模块使用。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

依赖注入

模块类也可以注入提供者(例如,用于配置目的):

@@filename(cats.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}
@@switch
import { Module, Dependencies } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
@Dependencies(CatsService)
export class CatsModule {
  constructor(catsService) {
    this.catsService = catsService;
  }
}

但是,由于循环依赖,模块类本身不能作为提供者注入。

全局模块

如果您必须在任何地方导入相同的模块集,这可能会变得乏味。与 Nest 不同,Angular providers 在全局范围内注册。一旦定义,它们就在任何地方都可用。然而,Nest 将提供者封装在模块范围内。如果不首先导入封装模块,您就无法在其他地方使用模块的提供者。

当您想要提供一组应该在任何地方都开箱即用的提供者(例如,助手、数据库连接等)时,请使用 @Global() 装饰器使模块成为全局模块。

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

@Global() 装饰器使模块成为全局作用域。全局模块应该只注册一次,通常由根模块或核心模块注册。在上面的示例中,CatsService 提供者将无处不在,希望注入该服务的模块不需要在其导入数组中导入 CatsModule

info 提示 不建议将所有内容都设为全局作为设计实践。虽然全局模块可以帮助减少样板代码,但通常最好使用 imports 数组以受控和清晰的方式使模块的 API 对其他模块可用。这种方法提供了更好的结构和可维护性,确保只有模块的必要部分与其他模块共享,同时避免应用程序不相关部分之间的不必要耦合。

动态模块

Nest 中的动态模块允许您创建可以在运行时配置的模块。当您需要提供灵活、可定制的模块,其中提供者可以基于某些选项或配置创建时,这特别有用。以下是动态模块工作原理的简要概述。

@@filename()
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}
@@switch
import { Module } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
  exports: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options) {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

info 提示 forRoot() 方法可以同步或异步(即通过 Promise)返回动态模块。

该模块默认定义了 Connection 提供者(在 @Module() 装饰器元数据中),但另外 - 根据传递给 forRoot() 方法的 entitiesoptions 对象 - 公开一组提供者,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)在 @Module() 装饰器中定义的基础模块元数据。这就是静态声明的 Connection 提供者动态生成的存储库提供者都从模块导出的方式。

如果您想在全局范围内注册动态模块,请将 global 属性设置为 true

{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

warning 警告 如上所述,将所有内容都设为全局不是一个好的设计决策

DatabaseModule 可以通过以下方式导入和配置:

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

如果您想反过来重新导出动态模块,可以在导出数组中省略 forRoot() 方法调用:

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class AppModule {}

动态模块章节更详细地涵盖了这个主题,并包含一个工作示例

info 提示此章节中了解如何使用 ConfigurableModuleBuilder 构建高度可定制的动态模块。