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
  ) { ... }
}
相关推荐
kongxx9 天前
NestJS中使用Guard实现路由保护
nestjs
白雾茫茫丶10 天前
Nest.js 实战 (十二):优雅地使用事件发布/订阅模块 Event Emitter
nestjs·nest.js·发布订阅·event emitter
lph65821 个月前
比起上传资源更应该懂得如何资源回收
node.js·nestjs
gsls2008081 个月前
将nestjs项目迁移到阿里云函数
阿里云·云计算·nestjs·云函数
d3126975101 个月前
在Nestjs使用mysql和typeorm
mysql·express·nestjs·typeorm
剪刀石头布啊2 个月前
nestjs-版本控制
nestjs
潇洒哥gg2 个月前
重生之我在NestJS中使用jwt鉴权
前端·javascript·nestjs
huangkaihao2 个月前
【NestJS学习笔记】 之 自定义装饰器
前端·node.js·nestjs
鹿鹿鹿鹿isNotDefined2 个月前
Nest 源码解析:依赖注入是怎么实现的?
nestjs
剪刀石头布啊2 个月前
nestjs-自定义装饰器
nestjs