白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 04:ConfigModule 与基础数据库生命周期

接入全局配置管理,统一读取环境变量。引入 DatabaseModule 和 DatabaseService。学会在 Nest 中管理资源生命周期(启动连接、退出释放)。

本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。

1. 这节课要实现的功能是什么

  • 接入全局配置管理,统一读取环境变量。
  • 引入 DatabaseModule 和 DatabaseService。
  • 学会在 Nest 中管理资源生命周期(启动连接、退出释放)。

2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)

2.1 为什么需要 ConfigModule

配置不是业务逻辑的一部分,但会影响运行行为。把配置统一管理,能避免魔法字符串散落全项目。

2.2 ConfigService 主要提供什么功能

ConfigService 是配置读取入口,核心价值是“统一、可控、可测试”:

  • 统一读取配置:不再到处直接写 process.env;
  • 类型化读取:get<number>('PORT', 3000) 这种写法更安全;
  • 支持默认值:缺失时不至于直接崩溃;
  • 支持命名空间配置:和 registerAs 配合后可按模块管理配置。

2.3 ConfigModule.forRoot 为什么要有 forRoot

forRoot 是 Nest 动态模块的初始化入口。
它的作用是在“应用启动阶段”接收配置选项,并据此创建配置相关 Provider。

常见理解方式是:
ConfigModule 提供能力,forRoot(...) 决定这次应用怎么初始化这套能力。

2.4 ConfigModule 可以动态或手动添加配置吗

可以,且很常见。你有两种主流方式:

  • 手动拼配置:forRoot({ load: [() => ({ ... })] })
  • 按模块拆配置:registerAs + forFeature

例如手动加载:

ConfigModule.forRoot({
  isGlobal: true,
  load: [
    () => ({
      app: { port: Number(process.env.PORT ?? 3000) },
      jwt: { expiresIn: '7d' },
    }),
  ],
});

2.5 isGlobal: true 的含义

设置 isGlobal: true 后,不需要在每个模块重复导入 ConfigModule,可直接注入 ConfigService。

2.6 生命周期钩子 OnModuleInit / OnModuleDestroy

  • OnModuleInit:模块就绪时执行,适合做连接初始化;
  • OnModuleDestroy:应用关闭时执行,适合做连接释放。

2.7 @Global() 模块该怎么用

@Global() 适合全局基础设施模块(配置、数据库、日志)。它减少重复导入,但不要滥用在业务模块上。

它的关键语义是:全局可注入,不是“完全不需要导入”。
你仍然要在应用里至少导入一次该模块(通常在 AppModule),之后其它模块才可以直接注入它导出的 Provider。

2.8 如果没有 @Global(),该怎么复用模块能力

如果一个模块没有 @Global(),那么任何想使用它导出 Provider 的模块,都必须显式写在 imports 里。

同时要满足两件事:

  1. 提供方模块在 exports 里导出了该 Provider;
  2. 使用方模块在 imports 里导入了提供方模块。

这就是 Nest 里标准的“模块可见性链路”。

2.9 为什么不直接在 providers 里重复声明依赖

很多初学者会问:既然我想用 DatabaseService,为什么不在当前模块 providers 里再写一遍?

原因是 providers 的语义是“本模块自己提供”。
你在不同模块重复声明同一个类,通常会得到不同实例,可能导致状态不一致(比如缓存、连接状态、内存数据)。

所以跨模块复用时,优先走 imports + exports,而不是复制 providers。

2.10 Provider 实例到底怎么创建

默认情况下(Scope.DEFAULT):

  • 同一个模块内,一个 Provider 通常是单例;
  • 不同模块各自声明同一个 Provider,会各自创建实例;
  • 通过 imports + exports 复用时,才是共享同一提供方实例。

如果显式使用 Scope.REQUEST 或 Scope.TRANSIENT,实例生命周期会变成“按请求”或“按注入点”创建。

2.11 多模块共享一个 Service 实例的标准模式

标准做法是:

  1. 在一个模块里 providers 并 exports;
  2. 其他模块只 imports,不重复声明同一个 Provider;
  3. 需要全局可注入时,再考虑 @Global()。

一句话总结:providers 是“我自己创建”,imports 是“我复用别人创建”。

3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)

3.1 src/app.module.ts - 引入全局配置与数据库模块

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
    }),
    DatabaseModule,
  ],
})
export class AppModule {}

3.2 src/database/database.service.ts - 用 ConfigService 读取配置并管理生命周期

import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(DatabaseService.name);
  private connected = false;

  constructor(private readonly configService: ConfigService) {}

  async onModuleInit(): Promise<void> {
    const driver = this.configService.get<string>('DB_DRIVER', 'in-memory');
    this.connected = true;
    this.logger.log(`Database initialized with driver: ${driver}`);
  }

  async onModuleDestroy(): Promise<void> {
    this.connected = false;
    this.logger.log('Database connection closed');
  }

  isConnected(): boolean {
    return this.connected;
  }
}

3.3 进阶示例:src/config/db.config.ts 声明 dbConfig

import { registerAs } from '@nestjs/config';

export const dbConfig = registerAs('db', () => ({
  host: process.env.DB_HOST ?? 'localhost',
  port: Number(process.env.DB_PORT ?? 5432),
  user: process.env.DB_USER ?? 'postgres',
  password: process.env.DB_PASSWORD ?? '',
  name: process.env.DB_NAME ?? 'app',
}));

3.4 进阶示例:ConfigModule.forFeature(dbConfig) 在模块内按需引入

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { dbConfig } from '../config/db.config';
import { DatabaseService } from './database.service';

@Module({
  imports: [ConfigModule.forFeature(dbConfig)],
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

这套写法适合中大型项目:配置按领域拆分,模块只加载自己关心的配置。

3.5 src/database/database.module.ts - 作为全局基础设施导出

import { Global, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Global()
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

3.6 跨模块共享同一个 DatabaseService 实例

DatabaseModule 被 @Global() 标记后,只要在 AppModule 导入一次,其他业务模块就能直接注入:

import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';

@Injectable()
export class UsersService {
  constructor(private readonly databaseService: DatabaseService) {}

  health() {
    return { dbConnected: this.databaseService.isConnected() };
  }
}

如果你把 @Global() 去掉,那么 UsersModule 需要显式导入:

@Module({
  imports: [DatabaseModule],
  providers: [UsersService],
})
export class UsersModule {}

注意:不要在 UsersModule 的 providers 里再次声明 DatabaseService,否则会变成新的本地实例。

3.7 .env.example - 约定配置项

PORT=3000
DB_DRIVER=in-memory
JWT_SECRET=dev-secret

3.8 本节落地价值

你现在拥有了标准的“配置中心 + 生命周期管理”基础设施。后续接 Prisma、Redis、MQ 时,模式完全一致。

4. 总结这节课知识点

  • ConfigModule 让配置可控、可测试、可切环境。
  • 你掌握了 ConfigService 的读取方式与类型化访问价值。
  • 你理解了 forRoot 是动态模块初始化入口。
  • 你知道了如何用 load、registerAs、forFeature 做配置扩展。
  • 生命周期钩子保证资源初始化与释放有明确边界。
  • 你理解了 @Global()、imports、providers 三者的职责边界。
  • 你掌握了多模块共享单实例 Provider 的标准实现方式。
  • DatabaseModule 作为基础设施模块,为后续真实数据库铺路。

5. 每节课分支

  • lesson-04-prisma-config
  • 远程仓库:https://github.com/Shiuwn/nestjs-tutorial.git

返回课程目录

上一课:Repository 抽象与可替换存储

下一课:注册登录与 JWT 鉴权

— 感谢阅读 —