Nestjs 学习记录:(二)Modules

一、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 的基本结构:

  1. ConfigModule 是一个包含静态方法 register 的类,因此可以直接从类本身调用。理论上方法名不作限制,但是遵循惯例一般命名为 register、forRoot、forFeature;
  2. register 可以接收任何参数来修改模块的功能;
  3. 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
  ) { ... }
}
相关推荐
亮子AI3 天前
【NestJS】为什么return不返回客户端?
前端·javascript·git·nestjs
小p4 天前
nestjs学习2:利用typescript改写express服务
nestjs
Eric_见嘉9 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu200218 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200219 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄20 天前
NestJS 调试方案
后端·nestjs
当时只道寻常23 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs