模块导出 provider,另一个模块需要 imports 它才能用这些 provider。
但如果这个模块被很多模块依赖了,那每次都要 imports 就很麻烦。
能不能设置成全局的,它导出的 provider 直接可用呢?
Module、Controller、Provider 是由 Nest 创建的,能不能在创建、销毁的时候执行一些逻辑呢?
这节我们来学习下全局模块和生命周期。
创建一个 nest 项目:
nest new global-and-lifecycle -p npm
然后创建两个 CRUD 的模块:
nest g resource aaa --no-spec
nest g resource bbb --no-spec
2
--no-spec 是不生成测试文件
在 AaaModule 里指定 exports 的 provider:
# 普通导入导出
然后在 BbbModule 里 imports:
这样就可以在 BbbModule 内注入 AaaService 了:
把 nest 服务跑起来:
npm run start:dev
可以看到 aaaService 生效了:
这是我们常用的引入 Module 的方式。
# 模块重导出
有时候我们希望在导出一个模块的同时,也重新导出它所导入的模块。这样可以让这个模块的消费者直接使用它依赖的模块的功能,而无需显式地导入它们。
在 Nest.js 中,可以使用 exports
数组来重导出模块:
@Module({
imports: [CommonModule],
providers: [EmailService],
exports: [EmailService, CommonModule] // 重导出 CommonModule
})
export class EmailModule {}
2
3
4
5
6
在上面的代码中,EmailModule
不仅导出了它自己的 EmailService
,也重导出了它导入的 CommonModule
。
这意味着导入 EmailModule
的模块也会自动导入 CommonModule
,并可以使用其提供的服务。
这种重导出机制在组织大型应用程序时非常有用,因为它可以减少模块间的耦合,同时简化外部模块的导入过程。
# 全局导入
但如果这个 AaaModule 被很多地方引用呢?
每个模块都 imports 太麻烦了,这时候就可以把它声明为全局的:
在 AaaModule 上加一个 @Global 的装饰器,然后在 BbbModule 里把 AaaModule 的 imports 去掉。
这样依然是可以注入的:
这就是全局模块。
注意:Nest 中只能定义一个全局模块!
不过全局模块还是尽量少用,不然注入的很多 provider 都不知道来源,会降低代码的可维护性。
将所有东西都放在全局模块内是一个不好的决定,全局模块只是用于减少必要的文件数量,
imports
仍然是使模块 API 透明的最佳方式。
笔记
在 NestJS 中,即使你将一个模块标记为全局模块(通过使用 @Global()
装饰器),它仍然需要在至少一个地方注册或导入,通常是在 AppModule
或者其他根模块中。这是因为 NestJS 需要有一个入口点来识别和加载这些全局模块。
# 动态模块
我们需要导入的时候给其传入一些参数怎么办?可以使用动态模块。
动态模块允许在模块被导入时动态配置提供者(providers)、控制器(controllers)和导出(exports)。
这种方式非常适合需要根据不同环境或配置动态更改其行为的模块。
下面是一个简单的例子:
多了一个 module
属性指向当前的模块类,即 DynamicModuleModule
。
我们给 DynamicModuleModule 加一个 register 的静态方法,返回模块定义的对象,外部传入的 options 参数对象会作为一个新的 provider。
import { Module, DynamicModule } from '@nestjs/common';
import { BbbService } from './bbb.service';
import { BbbController } from './bbb.controller';
@Module({})
export class BbbModule {
static register(options: Record<string, any>): DynamicModule {
const providers = [
BbbService,
{
provide: 'bbbOptions',
useValue: options,
},
];
return {
// global: true,
module: BbbModule,
controllers: [BbbController],
providers: providers,
exports: [BbbService, 'bbbOptions'],
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果要在全局范围内注册动态模块,请将
global
属性设置为true
。
导入时配置:
注入依赖:
# 生命周期
然后是生命周期:
Nest 在启动的时候,会递归解析 Module 依赖,扫描其中的 provider、controller,注入它的依赖。
全部解析完后,会监听网络端口,开始处理请求。
这个过程中,Nest 暴露了一些生命周期方法:
首先,递归初始化模块,会依次调用模块内的 controller、provider 的 onModuleInit
方法,然后再调用 module 的 onModuleInit
方法。
全部初始化完之后,再依次调用模块内的 controller、provider 的 onApplicationBootstrap
方法,然后调用 module 的 onApplicationBootstrap
方法
然后监听网络端口。
之后 Nest 应用就正常运行了。
这个过程中,onModuleInit
、onApplicationBootstrap
都是我们可以实现的生命周期方法。
我们来试一下:
再创建两个 Module:
nest g resource ccc --no-spec
nest g resource ddd --no-spec
******
nest 提供了这样两个 interface:
在 controller、service、module 里分别实现它:
ddd 模块也是这样。
然后重新跑下服务,会看到这样的日志信息:
这就是 onModuleInit 和 onApplicationBootstrap 生命周期的调用顺序。
应用销毁的时候也同样有生命周期:
先调用每个模块的 controller、provider 的 onModuleDestroy 方法,然后调用 Module 的 onModuleDestroy 方法。
之后再调用每个模块的 controller、provider 的 beforeApplicationShutdown 方法,然后调用 Module 的 beforeApplicationShutdown 方法。
然后停止监听网络端口。
之后调用每个模块的 controller、provider 的 onApplicationShutdown 方法,然后调用 Module 的 onApplicationShutdown 方法。
之后停止进程。
是不是感觉 onModuleDestory 和 beforeApplicationShutdown 没区别呀?
其实是有区别的,可以看下对应的 interface:
beforeApplicationShutdown 是可以拿到 signal 系统信号的,比如 SIGTERM。
这些终止信号是别的进程传过来的,让它做一些销毁的事情,比如用 k8s 管理容器的时候,可以通过这个信号来通知它。
我们分别给 CccController、CccProvider、CccModule 还有 ddd 模块的那些给加一下:
3s 后调用 app.close() 触发销毁(app.close() 只是触发销毁逻辑,但不会真正退出进程)
# 执行顺序
生命周期方法是这样的执行顺序:
# 异步生命周期
而且所有的生命周期函数都是支持 async 的。
我们来看看 @nestjs/typeorm、@nestjs/mongoose 里都是怎么用的:
可以看到,一般都是通过 moduleRef 取出一些 provider 来销毁,比如关闭连接。
这里的 moduleRef 就是当前模块的引用。
我们来试试:
onApplicationShutdown 的生命周期里,拿到当前模块的引用 moduleRef,调用 get 方法,传入 token,取出对应的 provider 实例,然后调用它的方法。
import { Module, OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { CccService } from './ccc.service';
import { CccController } from './ccc.controller';
@Module({
controllers: [CccController],
providers: [CccService]
})
export class CccModule implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown {
onModuleDestroy() {
console.log('CccModule onModuleDestroy');
}
beforeApplicationShutdown(signal: string) {
console.log('CccModule beforeApplicationShutdown', signal);
}
onApplicationShutdown() {
const cccService = this.moduleRef.get<CccService>(CccService);
console.log('--------------------------', cccService.findAll());
console.log('CccModule onApplicationShutdown');
}
onModuleInit() {
console.log('CccModule OnModuleInit');
}
onApplicationBootstrap() {
console.log('CccModule onApplicationBootstrap');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这就是 onApplicationShutdown 生命周期的常见用法。
案例代码在小册仓库 (opens new window)。
# 总结
这节我们学习了全局模块和生命周期。
模块可以通过 @Global 声明为全局的,这样它 exports 的 provider 就可以在各处使用了,不需要 imports。
provider、controller、module 都支持启动和销毁的生命周期函数,这些生命周期函数都支持 async 的方式。
可以在其中做一些初始化、销毁的逻辑,比如 onApplicationShutwon 里通过 moduleRef.get 取出一些 provider,执行关闭连接等销毁逻辑。
全局模块、生命周期、moduleRef 都是 Nest 很常用的功能。