一、Intro
每个 Nest 应用至少会有一个 root module【或 app moudule】,它会作为 Nest 构建用于解析 module 和 provider 间依赖关系的数据结构的起点。对于绝大多数应用,最终的结构将采用多个模块,每个模块封装一组密切相关的功能。
@Module() 装饰器接受一个对象,该对象的属性描述了该模块:
properties | description |
---|---|
providers | 由 Nest 注入器实例化的 providers 集合,它会被当前模块的所有类共享 |
controllers | controllers 集合 |
imports | 外部的其他模块,其中可能会有当前模块需要的 providers |
exports | providers 的子集,可供其他模块引入并使用 |
注意:如果 provider 既不是模块的直接组成部分,也不是从其他引入模块所导出的成员,是无法作为 provider 注入到当前模块的
二、Basic Example
第一步,定义 UserModule
,注入并导出 UserService
模块
ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
providers: [UsersService],
controllers: [UserController],
exports: [UsersService]
})
export class UsersModule {}
第二步,定义 AuthModule
,导入 UserModule
,使其能够获取到 UserModule 中导出的 providers
ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [AuthService]
})
export class AuthModule {}
第三步,在 AuthService
的构造函数中传入 UserService
。由于 AuthService 被 AuthModule 托管,所以它可以通过 Nest 的依赖注入拿到 UserService 的实例
ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
/*
Implementation that makes use of this.usersService
*/
}
三、Use Case
在章节二中,会涉及到两个模块:UserModule 和 AuthModule。本章节我们会基于它们来介绍模块的分类及应用场景。
(一)Feature Modules
功能模块主要是用于处理特定的功能,比如 UserService 和 UserController 都是与用户实体相关的类,在功能上关联密切,因此可以移动到同一个功能模块中。这样组织代码可以保证项目模块之间的边界更加明确。
arduino
// 自动创建模块
nest g module cats
// 自动创建服务
nest g s cats
// 自动创建控制器
nest g co cats
(二)Shared Modules
首先,Nest 中的模块默认使用单例模式,因此,多个模块可以共享一个 provider 的相同实例。其次,Nest 中的模块默认为 shared module,只要被创建就可以在不同模块间共享。
上述示例中,我们使用 exports: [UsersService] 和 imports: [UsersModule] 将 UserService 共享给 AuthModule,此时就可以通过 constructor(private usersService: UsersService) {} 进行依赖注入,并直接调用 this.UserService 使用它的内部方法。
(三)Global Modules
默认情况下,Nest 不支持在未引入模块的情况下使用它导出的 providers。想要实现全局模块,可以给模块额外添加 @Global() 装饰器
(四)Dynamic Modules
上述的 modules 仅指代静态模块。对于使用者而言,它们并没有可用的接口来影响从宿主模块中获取的 providers 是如何配置的。
针对这类需求, Configuration Module 是一个很典型的示例。当我们需要根据项目当前的部署环境使用不同的配置时【环境变量,比如数据库连接地址等】,可以把这些配置参数的管理交给 Configuration Module,保证了项目代码和配置参数之间的独立性。
关键点在于,作为一个通用的模块,对于不同的使用者需要表现出不同的行为,对于静态模块来说是无法实现的。因此,Nest 引入了 "动态模块" 的概念。
动态模块会对外提供方法接口,在导入该模块时由使用者自定义模块的属性和方法。
以下是一个使用动态模块的示例:
第一步,定义一个 ConfigModule ,它拥有一个 register 静态方法,可以传入配置参数并返回动态模块
ts
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options?: unknown): DynamicModule {
return {
// 可以通过 imports 配置项导入其他模块
module: ConfigModule,
providers: [ConfigService],
exports: [ConfigService],
};
}
}
第二步,导入 ConfigModule,调用其内部的 register 方法并传入相应参数
ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
@Module({
imports: [ConfigModule.register({ folder: './config' })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
从上述代码中,我们可以推断出 ConfigModule 的基本结构:
- ConfigModule 是一个包含静态方法 register 的类,因此可以直接从类本身调用。理论上方法名不作限制,但是遵循惯例一般命名为 register、forRoot、forFeature;
- register 可以接收任何参数来修改模块的功能;
- register 会在运行时动态地返回一个模块。
第三步,读取并使用用户传入的 options 配置。ConfigModule 仅作为导出各种服务的托管容器,具体的执行代码需要在 ConfigService 中编写。
我们先在 ConfigService 文件中写死关于 options 的代码。
这里的 ConfigService 只是演示如何使用 options,后续会修改:
ts
import { Injectable } from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
const options = { folder: './config' };
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
剩下的任务就是如何将 options 注入
到 ConfigService 中。
看到"注入",该怎么做其实就很明确了。在 Providers > Custom Providers 中,我们已经学习到 Provider 并不局限于 Service,可以代表任何值,因此,我们可以使用 useValue 将 options 作为一个 provider 来注入
ts
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({})
export class ConfigModule {
static register(options: Record<string, any>): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
};
}
}
ts
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
import { Injectable, Inject } from '@nestjs/common';
import { EnvConfig } from './interfaces';
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
// 使用 @Inject 装饰器实现基于属性的注入
constructor(@Inject('CONFIG_OPTIONS') private options: Record<string, any>) {
const filePath = `${process.env.NODE_ENV || 'development'}.env`;
const envFile = path.resolve(__dirname, '../../', options.folder, filePath);
this.envConfig = dotenv.parse(fs.readFileSync(envFile));
}
get(key: string): string {
return this.envConfig[key];
}
}
四、Community Guideline
在一些 @nestjs/ 的三方库中,我们经常会看到 register、forRoot、forFeature 等方法,我们可以了解其中的区别以更好地理解开发者的意图:
API | 描述 | 实例 |
---|---|---|
register / registerAsync |
使用特定的配置项创建 Dynamic Module | 例如 @nestjs/axios 可以使用 HttpModule.register({ baseUrl: 'someUrl' }) 配置axios的baseurl |
forRoot/forRootAsync |
仅配置一次动态模块,并在不同的模块中使用它 | 例如 TypeOrmModule.forRoot() |
forFeature/forFeatureAsync |
使用动态模块的 forRoot 配置的同时,希望能调整部分设置以适应不同模块的需求 |
五、Configurable Module Builder
Nest 提供了 ConfigurableModuleBuilder 类,来帮助开发者快速创建一个可配置的动态模块。这里直接按照官网提供的例子过一遍即可:
ts
export interface ConfigModuleOptions {
folder: string;
}
ts
import { ConfigurableModuleBuilder } from '@nestjs/common';
import {
ConfigModuleOptions
} from './interfaces/config-module-options.interface';
export const {
ConfigurableModuleClass,
MODULE_OPTIONS_TOKEN
} = new ConfigurableModuleBuilder<ConfigModuleOptions>().build();
ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigurableModuleClass } from './config.module-definition';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule extends ConfigurableModuleClass {}
ts
@Module({
imports: [
ConfigModule.register({ folder: './config' }),
// or alternatively:
// ConfigModule.registerAsync({
// useFactory: () => {
// return {
// folder: './config',
// }
// },
// inject: [...any extra dependencies...]
// }),
],
})
export class AppModule {}
ts
import { MODULE_OPTIONS_TOKEN } from './config.module-definition';
@Injectable()
export class ConfigService {
constructor(
@Inject(MODULE_OPTIONS_TOKEN) private options: ConfigModuleOptions
) { ... }
}