当系统需要支持 多租户数据库、动态配置中心或 插件化架构时,传统的固定依赖绑定将成为阻碍。本期我们深入NestJS依赖注入高级特性,解锁面向复杂业务场景的终极武器。
一、依赖注入进化论:从基础到高级
1. 基础依赖注入回顾
// 标准注入示例
@Controller()
export class UserController {
// 构造器注入
constructor(
private userService: UserService,
private logger: LoggerService
) {}
}
@Module({
providers: [UserService, LoggerService], // 提供器注册
})
export class AppModule {}- ✅ 简单直接
- ❌ 静态绑定缺乏灵活性
- ❌ 无法在运行时切换实现
2. 复杂场景下的依赖困境
假设你需要:
- 根据请求动态选择数据库(如多租户系统)
- 异步加载配置文件后才初始化服务
- 同一个接口支持多种实现(如支付网关)
graph LR
A[请求] -->|租户ID| B[动态选择DataSource]
B --> C[MySQL]
B --> D[MongoDB]
B --> E[PostgreSQL]二、动态模块开发:配置驱动的模块组装
1. 静态模块 vs 动态模块
| 特性 | 静态模块 | 动态模块 |
|---|---|---|
| 配置方式 | .forRoot({...}) | .forRootAsync({...}) |
| 初始化时机 | 启动时固定配置 | 支持异步加载配置 |
| 应用场景 | 基础组件注册 | 需要外部资源的模块 |
| 灵活度 | ⭐️⭐️ | ⭐️⭐️⭐️⭐️ |
2. 实现动态数据库模块
// database/database.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({})
export class DatabaseModule {
static forRootAsync(options: {
useFactory: (...args: any[]) => Promise<any> | any;
inject?: any[];
}): DynamicModule {
return {
module: DatabaseModule,
imports: [
TypeOrmModule.forRootAsync({
useFactory: options.useFactory,
inject: options.inject || [],
}),
],
};
}
}
// 使用场景:从配置服务加载数据库配置
@Module({
imports: [
ConfigModule.forRoot(),
DatabaseModule.forRootAsync({
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
type: 'mysql',
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production',
}),
}),
],
})
export class AppModule {}三、依赖作用域:掌控组件生命周期
1. NestJS支持的三种作用域
| 作用域 | 生命周期 | 典型应用场景 |
|---|---|---|
| DEFAULT | 单例 | 无状态服务 |
| TRANSIENT | 每次注入时新建 | 带临时状态的服务 |
| REQUEST | 每次请求新建 | 请求上下文依赖 |
// 声明作用域
@Injectable({ scope: Scope.REQUEST })
export class RequestLogger {
// 每个请求的独立实例
}
@Injectable({ scope: Scope.TRANSIENT })
export class TemporaryTaskService {
// 每次注入都生成新实例
}2. 请求作用域实战:多租户数据库选择
// tenants/tenant-aware.service.ts
import { REQUEST } from '@nestjs/core';
import { Scope, Injectable, Inject } from '@nestjs/common';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TenantAwareService {
private dataSource: DataSource;
constructor(
@Inject(REQUEST) private readonly req: Request,
private configService: ConfigService
) {}
// 根据请求头选择租户数据库
async getDataSource(): Promise<DataSource> {
if (this.dataSource) return this.dataSource;
const tenantId = this.req.headers['x-tenant-id'];
const config = this.configService.getTenantConfig(tenantId);
this.dataSource = new DataSource({
type: 'postgres',
host: config.dbHost,
port: config.dbPort,
username: config.dbUser,
password: config.dbPass,
database: config.dbName,
});
return this.dataSource.init();
}
}3. 自定义提供器:灵活的对象创建方式
const loggerProvider = {
provide: 'LOGGER',
useFactory: (config: ConfigService) => {
return config.loggerType === 'cloud' ?
new CloudLogger() :
new FileLogger();
},
inject: [ConfigService],
};
@Module({
providers: [loggerProvider],
})
export class LoggerModule {}四、综合实战:多数据库切换系统
需求场景:同时支持SQL和NoSQL
- 核心业务数据 -> MySQL存储 (事务需求)
- 日志行为数据 -> MongoDB存储 (灵活模式)
- 访问频控数据 -> Redis存储 (高速访问)
graph TD
Request --> AppService
AppService --> SQLService[(SQL MySQL)]
AppService --> NoSQLService[(NoSQL MongoDB)]
AppService --> CacheService[(Redis)]实现方案:抽象仓库层 + 动态注入
步骤1: 定义通用接口
// repositories/base.repository.interface.ts
export interface IBaseRepository<T> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
}步骤2: 实现不同数据库适配器
// repositories/user.mysql.repository.ts
@Injectable()
export class UserMySQLRepository implements IBaseRepository<User> {
constructor(@Inject('MYSQL_CONNECTION') private dataSource: DataSource) {}
async findById(id: string) {
return this.dataSource
.getRepository(User)
.findOneBy(id);
}
}
// repositories/log.mongo.repository.ts
@Injectable()
export class LogMongoRepository implements IBaseRepository<Log> {
constructor(@Inject('MONGODB_CONNECTION') private mongoClient: MongoClient) {}
async save(log: Log) {
const collection = this.mongoClient.db().collection('logs');
return collection.insertOne(log);
}
}步骤3: 配置提供器映射
// repositories/repositories.module.ts
@Module({
providers: [
{
provide: 'USER_REPOSITORY',
useClass: process.env.DB_TYPE === 'mongodb' ?
UserMongoRepository : UserMySQLRepository
},
{
provide: 'LOG_REPOSITORY',
useClass: LogMongoRepository
}
],
})
export class RepositoriesModule {}步骤4: 服务层使用统一接口
@Injectable()
export class UserService {
constructor(
@Inject('USER_REPOSITORY') private userRepo: IBaseRepository<User>,
@Inject('LOG_REPOSITORY') private logRepo: IBaseRepository<Log>
) {}
async createUser(dto: CreateUserDto) {
const user = new User(dto);
await this.userRepo.save(user);
// 记录日志到MongoDB
const log = new Log(`User created: ${user.id}`)
await this.logRepo.save(log);
return user;
}
}五、高级进阶:模块扩展模式
1. 策略模式:运行时动态选择服务实现
// strategy/payment.strategy.module.ts
@Injectable()
export class PaymentStrategy {
constructor(
private paypalService: PaypalService,
private stripeService: StripeService
) {}
getService(paymentMethod: string): PaymentProcessor {
switch(paymentMethod) {
case 'paypal': return this.paypalService;
case 'stripe': return this.stripeService;
default: throw new Error('Unsupported payment method');
}
}
}
// 控制器中使用
@Post('pay/:method')
payOrder(
@Param('method') method: string,
@Body() paymentDto: PaymentDto
) {
const processor = this.paymentStrategy.getService(method);
return processor.pay(paymentDto);
}2. 插件架构:实现热插拔模块
// plugins/plugin.manager.ts
@Injectable()
export class PluginManager {
private plugins = new Map<string, PluginModule>();
register(name: string, plugin: PluginModule) {
this.plugins.set(name, plugin);
}
executeAll(event: string, payload: any) {
Array.from(this.plugins.values()).forEach(plugin => {
if (plugin.handlers[event]) {
plugin.handlers[event](payload);
}
});
}
}
// 使用示例:注册插件
this.pluginManager.register('analytics', {
handlers: {
userRegistered: (user) => {
console.log(`New user: ${user.email} - Tracking...`);
}
}
});六、避坑指南:DI高级应用陷阱
❌ 问题1:循环依赖
// 问题场景
class A {
constructor(b: B) {}
}
class B {
constructor(a: A) {}
}
// 解决方案1:forwardRef
@Injectable()
class A {
constructor(@Inject(forwardRef(() => B)) b: B) {}
}
// 解决方案2:使用Injector手动延迟解析
constructor(private injector: Injector) {}
resolveB() {
return this.injector.get(B);
}❌ 问题2:REQUEST作用域与单例服务冲突
@Injectable({ scope: Scope.REQUEST })
class ReqService {} // 请求作用域
@Injectable()
class SingletonService {
// 错误:尝试注入请求作用域组件
constructor(private reqService: ReqService) {}
}
// 正确解法:1. 改为请求作用域服务
@Injectable({ scope: Scope.REQUEST })
class SingletonService {
// 现在安全
}❌ 问题3:异步提供器未就绪
// 错误:在主模块启动前等待异步提供器
await app.init();
// 正确:使用APP_INITIALIZER
{
provide: APP_INITIALIZER,
useFactory: (config: ConfigService) => () => config.load(),
multi: true,
inject: [ConfigService]
}七、性能优化:作用域的正确使用姿势
1. 各作用域内存开销对比
| 作用域 | 对象数量 | 适用情况 | 性能影响 |
|---|---|---|---|
| SINGLETON | 1个 | 98%场景 | ⭐️⭐️⭐️⭐️⭐️ |
| TRANSIENT | N次注入创建新实例 | 需要隔离状态 | ⭐️⭐️⚠️ |
| REQUEST | 每个请求1个 | 依赖请求上下文的场景 | ⚠️⚠️⚠️ 高负载慎用 |
黄金法则:默认使用SINGLETON,仅在必要时刻升级作用域
2. 请求作用域优化方案
@Injectable({ scope: Scope.REQUEST })
export class HeavyRequestService {
// 初始化的耗时操作会拖慢每个请求
async initResource() { /* 耗操作处理 */ }
}
// 优化:改为单例+延迟初始化
@Injectable()
export class OptimizedService {
// 在首个请求时初始化,后续请求复用资源
private resource: Promise<HeavyResource>;
getResource() {
if (!this.resource) {
this.resource = this.initResource();
}
return this.resource;
}
private initResource() {
// 资源初始化逻辑...
}
}八、下期预告:微服务架构拆解
《NestJS微服务实战:gRPC与Kafka分布式系统》 将探索:
➠ TCP/gRPC跨服务通信模式
➠ Kafka实现分布式事件总线
➠ 分布式事务的Saga设计模式
➠ 实战:金融账户系统的服务协作
挑战任务:
✍️ 尝试实现SQLite和PostgreSQL的动态切换,在评论区分享代码!
通过本期深度探索,你已掌握DI高级特性:
✅ 动态模块异步初始化配置
✅ 三种作用域的精妙控制
✅ 多数据库切换架构设计
✅ 模块插件化扩展技巧
✅ 循环依赖解决方案
架构价值:这些模式是构建大型商业应用的基础,适用于电商平台、SaaS系统、中台服务等各种复杂场景。
相关推荐
- NestJS 从入门到精通 11:综合实战 - 构建知乎风格的全栈社区平台(下) 2025-06-24
- NestJS 从入门到精通 10:综合实战 - 构建知乎风格的全栈社区平台(上) 2025-06-24
- NestJS 从入门到精通 09:性能调优大师 - 百万并发下的极致优化策略 2025-06-24
- NestJS 从入门到精通 08:安全架构实战 - 构建金融级应用的8重防护门 2025-06-24
- NestJS 从入门到精通 07:配置管理与部署实战 - 多环境与容器化方案 2025-06-24
评论0
暂时没有评论