nestjs学习8:认识模块Module(根模块/全局模块/动态模块)

44 阅读9分钟

根模块

首先问一个问题:所有的创建的模块都需要在AppModuleimport吗?

答案是否定的,只有下面三种情况需要在AppModule中引入。

1. AppModule 自身要使用这个模块的功能

如果 AppModule 里的组件(比如 AppController、AppService)需要用 AaaModule 导出的服务,那必须在 AppModule 的 imports 里加 AaaModule。

import { Module } from '@nestjs/common';
import { AaaModule } from './aaa/aaa.module';
import { AppService } from './app.service';

@Module({
  imports: [AaaModule], // 因为AppService要用到AaaService
  providers: [AppService],
})
export class AppModule {}

// app.service.ts
import { Injectable } from '@nestjs/common';
import { AaaService } from './aaa/aaa.service';

@Injectable()
export class AppService {
  // AppService依赖AaaService,所以AppModule必须导入AaaModule
  constructor(private readonly aaaService: AaaService) {}

  getHello(): string {
    return this.aaaService.doSomething(); // 调用AaaModule的功能
  }
}

如果 AppModule 里的任何 provider/controller 要用到 AaaModule 的功能,就必须在 AppModule 里导入它 —— 这和 BbbModule 要用就必须自己导入的逻辑完全一致,只是 AppModule 是根模块而已。

2. 注册全局生效的核心模块

有些模块本身是 “基础设施”,需要在应用启动时就初始化,哪怕 AppModule 自身不用,也得在 AppModule 里导入 —— 比如数据库模块(TypeOrmModule)、配置模块(ConfigModule)、认证模块(PassportModule)等。

这些模块的特点是:它们的初始化是应用启动的前提,必须在根模块导入才能生效,但这不代表其他模块能直接用(除非加 @Global)。

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // 数据库模块
import { BbbModule } from './bbb/bbb.module';

@Module({
  imports: [
    // 初始化数据库连接(应用启动的核心步骤)
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      database: 'test',
      // ...其他配置
    }),
    BbbModule,
  ],
})
export class AppModule {}

这里 TypeOrmModule.forRoot () 是 “全局初始化数据库连接”,必须在 AppModule 里导入 —— 否则整个应用连不上数据库。但如果 BbbModule 里要操作数据库表,还是要自己导入TypeOrmModule.forFeature([User]),这就是 根模块做全局初始化,子模块做局部使用

3:把子模块纳入应用的启动范围

// 错误写法:BbbModule没被任何模块导入
// app.module.ts
import { Module } from '@nestjs/common';

@Module({
  imports: [], // 没导入BbbModule
})
export class AppModule {}

// bbb.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('bbb')
export class BbbController {
  @Get()
  getHello() {
    return 'Hello Bbb!';
  }
}

此时访问/bbb接口会 404,因为 BbbModule 没被 AppModule 导入,NestJS 启动时根本没加载这个模块。

正确逻辑:AppModule 导入 BbbModule,是为了让 BbbModule 被应用识别并启动。

第二个问题:什么样的模块不需要在AppModule中import呢?

答案是:除了上面三种情况外,其余的模块其实都不需要在AppModule中引入,只有在需要它的模块中引入即可。

假设:

  • AaaModule:封装了 “用户积分计算” 的功能(AaaService)。
  • BbbModule:处理 “订单相关” 的业务,只有下单时需要计算积分,所以只有 BbbModule 需要用 AaaModule。
  • CccModule:处理 “商品展示” 的业务,完全用不到积分功能。

正确写法(按需导入

// aaa.module.ts
import { Module } from '@nestjs/common';
import { AaaService } from './aaa.service';

@Module({
  providers: [AaaService],
  // 关键:要把AaaService导出,其他Module才能用
  exports: [AaaService]
})
export class AaaModule {}

// bbb.module.ts
import { Module } from '@nestjs/common';
import { BbbService } from './bbb.service';
import { AaaModule } from '../aaa/aaa.module'; // 只在需要的地方导入

@Module({
  imports: [AaaModule], // 导入AaaModule
  providers: [BbbService],
  controllers: [BbbController]
})
export class BbbModule {}

// app.module.ts(根模块)
import { Module } from '@nestjs/common';
import { BbbModule } from './bbb/bbb.module';
import { CccModule } from './ccc/ccc.module';
// 这里不用导入AaaModule,因为只有BbbModule需要

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

如果把 AaaModule 也加到 AppModule 的 imports 里,虽然功能能跑,但就像你在前端根组件里导入了一个只有某个子组件才用的小工具,完全没必要,还会让根模块变臃肿。

全局模块

模块导出 provider,另一个模块需要 imports 它才能用这些 provider。

但如果这个模块被很多模块依赖了,那每次都要 imports 就很麻烦。

能不能设置成全局的,它导出的 provider 直接可用呢?

在 AaaModule 里指定 exports 的 provider:

image.png

然后在 BbbModule 里 imports:

image.png

这样就可以在 BbbModule 内注入 AaaService 了:

image.png

这是我们常用的引入 Module 的方式。

但如果这个 AaaModule 被很多地方引用呢?

每个模块都 imports 太麻烦了,这时候就可以把它声明为全局的:

image.png

在 AaaModule 上加一个 @Global 的装饰器,然后在 BbbModule 里把 AaaModule 的 imports 去掉。

image.png

这样依然是可以注入的。

这就是全局模块。

不过全局模块还是尽量少用,不然注入的很多 provider 都不知道来源,会降低代码的可维护性。

动态模块

前面讲过,Provider 是可以通过 useFactory 动态产生的,那 Module 可不可以呢?

自然是可以的。

image.png

这个模块是静态的,也就是它的内容是固定不变的,每次 import 都是一样。

有的时候我们希望 import 的时候给这个模块传一些参数,动态生成模块的内容,怎么办呢?

这时候就需要 Dynamic Module 了:

import { DynamicModule, Module } from '@nestjs/common';
import { BbbService } from './bbb.service';
import { BbbController } from './bbb.controller';

@Module({})
export class BbbModule {

  static register(options: Record<string, any>): DynamicModule {
    return {
      module: BbbModule,
      controllers: [BbbController],
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        BbbService,
      ],
      exports: []
    };
  }
}

我们给 BbbModule 加一个 register 的静态方法,返回模块定义的对象。

和在装饰器里定义的时候的区别,只是多了一个 module 属性:

image.png

而且我们还可以把参数传入的 options 对象作为一个新的 provider。

import 的时候就得这样用了,通过 register 方法传入参数,返回值就是模块定义:

image.png

这时候我们把传入的 options 通过 useValue 创建了个 provider,这样模块内部就可以注入它了。

image.png

我在 BbbController 里面通过 token 注入这个 provider,打印下它的值。

改一下 register 的参数:

image.png

浏览器再访问下,可以看到控制台打印了 config 对象。

这样我们就可以在 import 一个模块的时候,传入参数,然后动态生成模块的内容。

这就是 Dynamic Module。

这里的 register 方法其实叫啥都行,但 nest 约定了 3 种方法名:

  • register
  • forRoot
  • forFeature

我们约定它们分别用来做不同的事情:

  • register:用一次模块传一次配置,比如这次调用是 BbbModule.register({aaa:1}),下一次就是 BbbModule.register({aaa:2}) 了
  • forRoot:配置一次模块用多次,比如 XxxModule.forRoot({}) 一次,之后就一直用这个 Module,一般在 AppModule 里 import
  • forFeature:用了 forRoot 固定了整体模块,用于局部的时候,可能需要再传一些配置,比如用 forRoot 指定了数据库链接信息,再用 forFeature 指定某个模块访问哪个数据库和表。

光这么说可能不够直观,我们看一个真实的动态模块就懂了。

比如 @nestjs/typeorm 的动态模块:

image.png

forRoot 传入配置,然后调用了TypeOrmCoreModule模块的forRoot方法:

image.png

它动态产生 provider 和 exports,返回模块定义。

forFeature 则是传入局部的一些配置,来动态产生局部用的模块:

image.png

typeorm 的模块用起来是这样的:

image.png

image.png

在 AppModule 里 import 通过 forRoot 动态产生的模块,在具体的业务 Module 里,通过 forFeature 传入具体实体类的配置。

其实 forRoot、forFeature、register 有区别么?

本质上没区别,只是我们约定了它们使用上的一些区别

ModuleRef 动态导入模块

看下这段代码:

import { Injectable } from '@nestjs/common';
import { PayService } from './pay.service';

@Injectable()
export class OrderService {
  # 常规注入:Nest项目一启动(npm run start),PayService 就被创建实例
  # 不管你调没调 createOrder 方法,不管用户付没付款,它都占着服务器内存
  constructor(private readonly payService: PayService) {}

  async createOrder(pay: boolean) {
    if (!pay) {
      return '无需支付'; # 就算走这个分支,PayService 也早就加载了
    }
    return this.payService.pay();
  }
}

PayService 是通过构造函数注入的,如果后面这个createOrder方法从来没被调用过,PayService 也会在 Nest 启动时被创建好,占着服务器的内存资源。

Nest 项目一启动,所有你通过构造函数常规注入的服务,都会被一次性创建好实例,放进内存里等着用 —— 哪怕这个服务从头到尾都没被调用过,它也不会自己消失,一直占着内存。

这也是为什么有时候模块多了,Nest 启动会慢一点,因为它要把所有常规注入的服务都先 “初始化” 一遍,就像前端项目启动时要加载所有 import 的模块一样。

这个时候就可以使用ModuleRef,你可以把它理解为前端的import()动态引入:

import { Injectable, ModuleRef } from '@nestjs/common';
import { PayService } from './pay.service';

@Injectable()
export class OrderService {
  // 第一步:先注入 ModuleRef 这个“工具”
  constructor(private readonly moduleRef: ModuleRef) {}

  // 下单方法:只有用户选择支付时,才获取 PayService
  async createOrder(pay: boolean) {
    // 1. 如果用户不支付,就不用加载 PayService,节省资源
    if (!pay) {
      return '订单创建成功,无需支付';
    }

    // 2. 只有需要支付时,才通过 ModuleRef “动态拿” PayService 实例
    // 相当于前端的 import('echarts'),用到才加载
    const payService = this.moduleRef.get(PayService);
    // 调用支付方法
    return payService.pay();
  }
}

上面this.moduleRef.get(PayService)生效的前提是,需要在订单模块的@module中 import 支付模块。

@Module({
  imports: [PayModule], // 必须加这一步!
  providers: [OrderService],
})

ModuleRef 还有一个核心场景:创建服务的新实例(突破单例限制)。

假设你有个 TaskService(任务服务),每个任务需要独立的实例(比如处理不同的任务参数,避免互相影响):

import { Injectable, ModuleRef } from '@nestjs/common';
import { TaskService } from './task.service';

@Injectable()
export class TaskManagerService {
  constructor(private readonly moduleRef: ModuleRef) {}

  // 处理多个任务:每个任务一个独立的 TaskService 实例
  async handleTasks(taskList: any[]) {
    const results = [];
    for (const task of taskList) {
      // resolve():每次调用都新建一个 TaskService 实例
      // 相当于前端调用 createUser(),每次都是新的
      const taskService = await this.moduleRef.resolve(TaskService);
      // 每个实例处理自己的任务,互不干扰
      results.push(await taskService.handle(task));
    }
    return results;
  }
}

默认情况下,不管你调多少次 TaskService,都是同一个 “对象”,改了这个对象的属性,所有地方都会受影响。

ModuleRef.resolve(),每次都会新建一个 TaskService 实例,就像前端每次调用 createUser() 都得到新对象,各个任务之间完全隔离,不会互相干扰。

moduleRef.get()已存在的单例实例moduleRef.resolve() 新建全新的实例。所以这里使用了ModuleRef.resolve()

forwardRef 模块的循环依赖

创建两个 Module, 然后这两个 Module 相互引用。

nest g module aaa
nest g module bbb

image.png

image.png

把服务跑起来,会报这样的错误:

image.png

意思是在解析 BbbModule 的时候,它的第一个 imports 是 undefined。

这有两个原因,一个是这个值本来就是 undefined,第二个就是形成了循环依赖。

因为 Nest 创建 Module 的时候会递归创建它的依赖,而它的依赖又依赖了这个 Module,所以没法创建成功,拿到的就是 undefined。

image.png

那怎么办呢?

其实我们可以先单独创建这两个 Module,然后再让两者关联起来。

也就是用 forwardRef 的方式:

image.png

image.png

这时候就没有错误了。

nest 会单独创建两个 Module,之后再把 Module 的引用转发过去,也就是 forwardRef 的含义。

image.png

除了 Module 和 Module 之间会循环依赖以外,provider 之间也会。

比如 Service 里可以注入别的 Service,自身也可以用来注入。

所以也会有循环引用。

这时候也是通过 forwardRef 解决。

image.png

image.png