本文属于 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 里。
同时要满足两件事:
- 提供方模块在
exports里导出了该 Provider; - 使用方模块在
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 实例的标准模式
标准做法是:
- 在一个模块里
providers并exports; - 其他模块只
imports,不重复声明同一个 Provider; - 需要全局可注入时,再考虑
@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