Skip to content
第 5 章 后端 ⏱ 13 分钟阅读

第 5 章:模块 ​

学习目标 ​

  • 理解 Module 的组织作用
  • 掌握 imports/exports/providers/controllers
  • 学会全局模块和动态模块
  • 避开 4 个模块拆分坑

一、Module 是干嘛的 ​

NestJS 用 Module 把 Controller、Service、其它 Module 组织起来,一个应用就是 Module 树。

typescript
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],          // 暴露给其它 Module
})
export class UsersModule {}

根模块:

typescript
// app.module.ts
@Module({
  imports: [UsersModule],
})
export class AppModule {}

二、四大配置项 ​

配置含义
imports引入其他 Module,才能用它们导出的 Provider
controllers当前 Module 的 Controller
providers当前 Module 的 Provider
exports当前 Module 哪些 Provider 可被其他 Module 注入

三、模块导出与共享 ​

typescript
// common.module.ts
@Module({
  providers: [LoggerService],
  exports: [LoggerService],         // 关键:不导出别人用不了
})
export class CommonModule {}

// users.module.ts
@Module({
  imports: [CommonModule],          // 拉进来才能注入
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(private readonly logger: LoggerService) {}
  //           ↑ 来自 CommonModule,因为 CommonModule exports 了它
}

⚠️ 坑 1:Service 写在 A 模块,要从 A 模块 exports 出来,B 模块 imports: [A],B 才能注入。

Provider 消费路径(谁可以注入谁):

  • 同 Module 的 Controller/Service —— 自动注入,不用任何配置(providers 注册即可)
  • 跨 Module 的 Controller/Service —— 需 exports(源 Module) + imports(目标 Module)
  • Controller 本身 —— 不需要在 providers 注册,通过构造器注入消费 Service 即可
┌─ users.module.ts ─────────────────┐
│ providers: [UsersService]        │
│ exports:   [UsersService]  ← 跨模块开关│
└────────────────────────────────────┘
                ↓ exports
┌─ orders.module.ts ────────────────┐
│ imports: [UsersModule]     ← 拉进来│
│ providers: [OrdersService]        │
└────────────────────────────────────┘
                ↓ OrdersService 可注入 UsersService

反例:logger.module.ts 没 exports: [LoggerService],其他 module 即使 imports: [LoggerModule] 也注入不了,NestJS 会报错:Nest can't resolve dependencies of the XxxService。

四、全局模块(@Global) ​

typescript
import { Global, Module } from '@nestjs/common';

@Global()
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

全局模块的 Provider 不用每个 Module 都 imports,直接注入即可。

⚠️ 坑 2:@Global 用太多会让依赖关系混乱 → 只给确实到处用的东西加(配置、日志、Redis Client)。

五、动态模块(传参配置) ​

typescript
// config.module.ts
@Module({})
export class ConfigModule {
  static register(options: { env: 'dev' | 'prod' }): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        { provide: 'CONFIG_OPTIONS', useValue: options },
        ConfigService,
      ],
      exports: [ConfigService],
    };
  }
}

// app.module.ts
@Module({
  imports: [
    ConfigModule.register({ env: 'prod' }),
  ],
})
export class AppModule {}

常用于 TypeOrmModule.forRoot({...})、JwtModule.register({...}) 这类需要传参的模块。

providers 数组里能放什么? —— 两类:

typescript
providers: [
  // 简写:直接写类(隐式 = { provide: UsersService, useClass: UsersService })
  UsersService,

  // 完整写法:对象,显式指定 provide + 来源
  { provide: 'CONFIG_OPTIONS', useValue: options },
]

对象形式的 4 种组合:

typescript
providers: [
  // 1. useValue —— 注入固定值(配置、Mock、第三方实例)
  { provide: 'API_KEY', useValue: 'sk-xxxxx' },

  // 2. useClass —— 注入类(可换实现,常用于测试)
  { provide: UsersService, useClass: process.env.NODE_ENV === 'test' ? MockUsersService : UsersService },

  // 3. useFactory —— 工厂函数返回(可注入其他 Provider)
  { provide: 'DB', useFactory: (cfg: ConfigService) => createPool(cfg.get('DB_URL')), inject: [ConfigService] },

  // 4. useExisting —— 复用已有 Provider(换个名字)
  { provide: 'ALIAS_USERS', useExisting: UsersService },
]

为什么需要对象形式? 因为不是所有 Provider 都是类 —— 配置、字符串 token、已 new 好的 axios 实例、Mock 对象,这些都不是类,必须用对象形式才能注入。

记忆口诀:

  • [类名] —— 简单场景,注入自己的类
  • [{ provide, useValue/useClass/useFactory }] —— 复杂场景,需要字符串 token / 换实现 / 工厂

动态模块里常见用法 —— 传参 + useValue 注入配置:

typescript
{
  module: ConfigModule,
  providers: [
    { provide: 'CONFIG_OPTIONS', useValue: options }, // ← 把外部传的 options 包成 Provider
    ConfigService,                                     // ← ConfigService 才能注入它
  ],
  exports: [ConfigService],
}

完整使用示例:

typescript
// config.service.ts
@Injectable()
export class ConfigService {
  // 注入动态模块里 useValue 包好的配置
  constructor(
    @Inject('CONFIG_OPTIONS') private readonly options: { env: 'dev' | 'prod' },
  ) {}

  get isProd(): boolean {
    return this.options.env === 'prod';
  }

  get env(): string {
    return this.options.env;
  }
}

// config.module.ts
@Module({})
export class ConfigModule {
  static register(options: { env: 'dev' | 'prod' }): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        { provide: 'CONFIG_OPTIONS', useValue: options },
        ConfigService,
      ],
      exports: [ConfigService],  // 导出,其他模块才能用
    };
  }
}

// app.module.ts
@Module({
  imports: [
    ConfigModule.register({ env: 'prod' }),  // ← 注册时传参
  ],
})
export class AppModule {}

// users.module.ts
@Module({
  imports: [ConfigModule.register({ env: 'prod' })],  // 需要的话再注册一次
  providers: [UsersService],
})
export class UsersModule {}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(private readonly config: ConfigService) {}

  someMethod() {
    if (this.config.isProd) { // ← 拿到动态模块里的配置
      // 生产环境逻辑
    }
  }
}

六、模块懒加载(可选) ​

typescript
import { LazyModuleLoader } from '@nestjs/core';

constructor(private readonly loader: LazyModuleLoader) {}

async onModuleInit() {
  const modRef = await this.loader.load(() => import('./heavy.module').then(m => m.HeavyModule));
  // 只在第一次调用时才初始化 HeavyModule
}

适合启动慢的模块,按需加载。

完整使用示例:

typescript
// heavy.module.ts —— 一个启动慢的模块(比如连多个外部服务)
@Module({
  providers: [HeavyService],
  exports: [HeavyService],
})
export class HeavyModule {}

// app.service.ts —— 在某个 Service 里按需加载
@Injectable()
export class AppService implements OnModuleInit {
  constructor(private readonly loader: LazyModuleLoader) {}

  private heavyServiceRef: ModuleRef | null = null;

  async onModuleInit() {
    // 应用启动时不会执行,只有第一次调用时才加载
    console.log('AppModule 已就绪');
  }

  async useHeavyFeature() {
    if (!this.heavyServiceRef) {
      // 第一次调用时,才异步加载 HeavyModule
      this.heavyServiceRef = await this.loader.load(() =>
        import('./heavy.module').then(m => m.HeavyModule),
      );
    }
    // 从加载好的 ModuleRef 里取出 HeavyService
    const heavyService = this.heavyServiceRef.get(HeavyService);
    return heavyService.doSomething();
  }
}

关键点:

  1. import('./heavy.module') —— 用动态 import(),代码会被 Webpack/Vite 打成单独 chunk
  2. 缓存在 heavyServiceRef —— 第二次调用直接复用,不再重新加载
  3. ModuleRef.get(HeavyService) —— 从懒加载的模块里拿 Provider,跟普通模块一样注入

适用场景:

  • 重报表模块(初始化要跑几分钟)
  • 多租户场景(每个租户独立模块)
  • 插件化系统(不同客户启用不同模块)
  • 微前端 / 微服务编排

对比 imports: [HeavyModule]:

方式启动时间首次调用适用
imports: [HeavyModule]慢(一起加载)快HeavyModule 必须立即用
LazyModuleLoader.load()快(按需)慢(首次要加载)重型模块、插件系统

七、模块拆分原则 ​

按业务域拆分,而不是按技术层。

src/
├── modules/
│   ├── users/         # 用户域
│   │   ├── users.module.ts
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   └── dto/
│   ├── orders/        # 订单域
│   └── products/      # 商品域
├── common/            # 公共(全局)
│   ├── logger.module.ts
│   └── config.module.ts
└── app.module.ts

⚠️ 坑 3:按 controllers/services/repositories 分目录 → 改一个功能要跳 5 个文件夹。

八、特性模块(Feature Module) ​

把每个业务当独立特性模块,通过 imports 拼装。

typescript
// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRoot({ /* ... */ }),
    UsersModule,
    OrdersModule,
    ProductsModule,
  ],
})
export class AppModule {}

⚠️ 坑 4:imports 顺序不影响,但循环引用 Module 会启动失败 → 用 forwardRef。

九、实战:组织一个电商骨架 ​

typescript
// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({ useFactory: () => ({ /* ... */ }) }),
    UsersModule,
    ProductsModule,
    OrdersModule,
  ],
})
export class AppModule {}

每个业务子模块各自 controllers + providers + imports 子依赖,导出对外的 Service。

十、本章小结 ​

要点关键
作用组织代码、隔离依赖、暴露 Provider
导出exports: [Service] 才能跨模块用
全局@Global() 跳过 imports,但要慎用
动态模块XxxModule.register({}) 用于传参
拆分按业务域,不按技术层

动手练习 ​

  1. 拆模块:把 Users 拆到 src/modules/users/,根模块 import 它
  2. 全局 Config:写一个 ConfigModule,用 @Global 暴露 ConfigService
  3. 动态模块:让 ConfigModule.register({ env }) 把 env 注入 Service

下一章:第 6 章:中间件 →

本站基于 VitePress 构建 · 由 StackHub 团队维护