4. 模块化设计原则:Feature Module 与 Shared Module 划分
在 NestJS 中,模块是组织代码的核心单元。合理的模块划分不仅影响代码的可维护性,更直接决定了事务边界的清晰度与 API 设计的稳定性。本章将深入探讨 Feature Module 与 Shared Module 的划分原则、模块的导入导出机制,以及全局模块与动态模块的高级用法。
4.1 Feature Module 与 Shared Module 的职责边界
NestJS 的模块体系可以划分为两大类:Feature Module(特性模块) 和 Shared Module(共享模块)。两者的职责对比如下:
| 维度 | Feature Module | Shared Module |
|---|---|---|
| 核心职责 | 封装特定业务领域(如订单、用户、支付) | 提供跨模块复用的基础设施(如数据库连接、日志、工具类) |
| 事务边界 | 通常拥有独立的数据源或聚合根,事务范围明确 | 不持有业务事务,仅提供无状态服务或单例资源 |
| 导出策略 | 按需导出 Service、Controller、Entity | 导出 Provider、Guard、Interceptor、Pipe 等通用组件 |
| 生命周期 | 随业务迭代频繁变化 | 相对稳定,变更频率低 |
| 典型示例 | OrderModule、UserModule、PaymentModule |
DatabaseModule、LoggerModule、ConfigModule |
4.2 模块的导入导出机制
NestJS 的模块系统通过 imports、exports、providers、controllers 四个核心属性来组织依赖关系。理解导入导出机制是构建可维护模块体系的基础。
4.2.1 基本导入导出规则
- 模块 A 导入模块 B:A 可以访问 B 中
exports导出的 Provider。 - 模块 B 导出 Provider:只有 B 的
exports数组中列出的 Provider 才能被其他模块使用。 - 循环依赖:NestJS 支持通过
forwardRef解决循环依赖,但应尽量避免。
// user.module.ts - Feature Module 示例
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { UserEntity } from './entities/user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])], // 导入 TypeORM 特性
controllers: [UserController],
providers: [UserService],
exports: [UserService], // 仅导出 UserService,供其他模块使用
})
export class UserModule {}
// database.module.ts - Shared Module 示例
import { Module, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseConfigService } from './database-config.service';
@Global() // 全局模块,无需在每个模块中重复导入
@Module({
imports: [
TypeOrmModule.forRootAsync({
useClass: DatabaseConfigService,
}),
],
providers: [DatabaseConfigService],
exports: [DatabaseConfigService],
})
export class DatabaseModule {}
4.2.2 导入导出机制的事务边界影响
在 API Design 驱动的事务治理中,模块的导入导出直接决定了事务的传播范围:
- 跨模块事务:如果模块 A 导入了模块 B 的 Service,并在同一个方法中调用 B 的数据库操作,事务边界会跨越两个模块。此时需要明确事务的传播行为(REQUIRED、REQUIRES_NEW 等)。
- 模块内事务:推荐将事务边界控制在单个 Feature Module 内部,跨模块调用时通过 API 网关或事件驱动解耦。
@Transactional() 装饰器明确标注事务边界;Shared Module 中的 Service 不应直接参与业务事务,而是通过回调或事件机制与业务层交互。
4.3 全局模块与动态模块
全局模块和动态模块是 NestJS 模块系统的高级特性,在大型项目中合理使用可以显著提升架构的灵活性和可维护性。
4.3.1 全局模块(Global Module)
使用 @Global() 装饰器标记的模块会自动注册到全局作用域,其导出的 Provider 可以被所有模块直接使用,无需在 imports 中重复导入。
| 特性 | 说明 | 适用场景 |
|---|---|---|
| 自动注册 | 全局模块只需在根模块导入一次 | 数据库连接、日志、配置中心 |
| 单例保证 | 全局模块的 Provider 在整个应用中是单例 | 缓存服务、事件总线 |
| 事务风险 | 全局模块的 Service 可能被多个模块注入,事务边界容易模糊 | 谨慎用于业务逻辑,优先用于基础设施 |
4.3.2 动态模块(Dynamic Module)
动态模块允许在运行时根据配置参数动态注册 Provider,是实现可配置化、插件化架构的关键技术。
// config.module.ts - 动态模块示例
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
export interface ConfigModuleOptions {
path: string;
isGlobal?: boolean;
}
@Module({})
export class ConfigModule {
static register(options: ConfigModuleOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
ConfigService,
],
exports: [ConfigService],
global: options.isGlobal ?? false, // 支持动态设置全局性
};
}
}
// 使用方式
// @Module({
// imports: [ConfigModule.register({ path: '.env', isGlobal: true })],
// })
4.4 模块划分与事务边界的映射关系
为了直观展示模块划分如何影响事务边界,下图描述了典型 NestJS 应用的模块拓扑与事务传播路径:
4.5 模块设计检查清单
在实际项目中,可以使用以下清单来评估模块设计的合理性:
| 检查项 | 通过标准 | 常见反例 |
|---|---|---|
| 单一职责 | 每个模块只封装一个业务聚合根 | 一个模块同时处理订单和用户 |
| 事务边界清晰 | 模块内事务范围明确,不跨模块传播 | Service 中同时调用两个模块的 Repository |
| 导出最小化 | 只导出必要的 Provider,隐藏内部实现 | 导出所有 Provider,包括内部工具类 |
| 全局模块节制 | 全局模块数量不超过 3 个 | 将业务模块也声明为全局模块 |
| 动态模块可配置 | 动态模块支持运行时参数注入 | 动态模块硬编码配置,失去动态意义 |
4.6 动态模块在事务治理中的高级应用
动态模块不仅可以用于配置化,还可以在事务治理中实现按需注册事务管理器:
// transaction.module.ts - 动态事务模块
import { DynamicModule, Module } from '@nestjs/common';
import { TransactionService } from './transaction.service';
export interface TransactionModuleOptions {
isolationLevel?: 'READ_COMMITTED' | 'REPEATABLE_READ' | 'SERIALIZABLE';
timeout?: number;
}
@Module({})
export class TransactionModule {
static forRoot(options: TransactionModuleOptions = {}): DynamicModule {
return {
module: TransactionModule,
providers: [
{
provide: 'TRANSACTION_OPTIONS',
useValue: options,
},
TransactionService,
],
exports: [TransactionService],
global: true,
};
}
static forFeature(options: TransactionModuleOptions = {}): DynamicModule {
return {
module: TransactionModule,
providers: [
{
provide: 'TRANSACTION_OPTIONS',
useValue: options,
},
TransactionService,
],
exports: [TransactionService],
};
}
}
// 在根模块中设置全局默认事务隔离级别
// @Module({
// imports: [TransactionModule.forRoot({ isolationLevel: 'READ_COMMITTED' })],
// })
// 在特定 Feature Module 中覆盖事务配置
// @Module({
// imports: [TransactionModule.forFeature({ isolationLevel: 'SERIALIZABLE' })],
// })
forRoot 和 forFeature 的区分,可以在全局层面设定默认事务策略,同时在特定业务模块中按需调整隔离级别,实现精细化的 API 事务治理。
4.7 总结
模块化设计是 NestJS 架构的基石,也是 API Design 驱动事务边界治理的第一道防线。通过合理划分 Feature Module 与 Shared Module、掌握导入导出机制、审慎使用全局模块与动态模块,可以构建出事务边界清晰、易于维护和扩展的应用架构。下一层我们将深入探讨 Provider 的作用域与依赖注入对事务传播的影响。