首页 / NestJS 入门教程 / 模块 Module

NestJS 入门教程

模块 Module

本教程共 47 篇 · 第 5 篇 · 更新于 2026-08-09 · 约 10 分钟阅读

NestJSModule模块依赖注入全局模块循环依赖动态模块

本节目标:掌握 NestJS 模块系统的核心机制,学会用模块组织代码、控制依赖、共享服务。

模块是什么

模块是 NestJS 组织代码的基本单元。

打个比方:你的应用是一栋大楼,模块就是里面的一个个房间。每个房间有自己的功能(用户管理、订单处理、认证鉴权),房间之间通过门(导入导出)互通。

每个 NestJS 应用至少有一个模块——根模块 AppModule。它是整个应用的起点,NestJS 从它开始构建依赖关系图。

@Module 装饰器

@Module() 装饰器来定义一个模块,它接收一个配置对象:

@Module({
  imports: [],      // 导入其他模块
  controllers: [],  // 注册本模块的控制器
  providers: [],    // 注册本模块的服务
  exports: [],      // 导出服务,让别的模块能用
})
export class AppModule {}

四个属性,各有各的分工:

属性作用比喻
imports导入别的模块打开门,借用别的房间的东西
controllers注册控制器这个房间的前台接待
providers注册服务这个房间里的干活人
exports导出服务把房间里的某个人借给别的房间用
Note

模块默认是”封装”的——别的模块不能直接用你的服务,除非你显式 exports 出去。这和 Angular 的设计思路一样。

创建功能模块

小项目只有一个 AppModule 就够了。项目一大,你得按功能拆分模块。

用 CLI 创建一个用户模块:

nest g module users

生成的代码很简单:

import { Module } from '@nestjs/common';

@Module({})
export class UsersModule {}

然后把控制器和服务加进去:

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

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

最后在根模块里导入它:

import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}
Tip

nest g module 创建模块时,CLI 会自动帮你把 UsersModule 加到 AppModuleimports 里。手动创建的话别忘了这一步。

模块的导入与导出

这是模块系统最核心的机制。

假设你有一个 AuthService,它需要用 UsersService 来查询用户信息。UsersServiceUsersModule 里,AuthServiceAuthModule 里。怎么让 AuthModule 用上 UsersService

第一步:UsersModule 导出 UsersService

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],  // 关键:导出
})
export class UsersModule {}

第二步:AuthModule 导入 UsersModule

@Module({
  imports: [UsersModule],  // 关键:导入
  providers: [AuthService],
})
export class AuthModule {}

这样 AuthService 就能通过构造函数注入 UsersService 了:

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
}

整个流程可以这样理解:

UsersModule                    AuthModule
┌──────────────────┐          ┌──────────────────┐
│ providers:       │          │ imports:         │
│   UsersService   │──导出──→│   [UsersModule]  │
│ exports:         │          │ providers:       │
│   [UsersService] │          │   AuthService    │
└──────────────────┘          └──────────────────┘

                              AuthService 可以
                              注入 UsersService
Note

如果 UsersModule 没有 exports: [UsersService]AuthModule 是拿不到 UsersService 的。很多人刚开始会忘记导出,然后发现注入报错——八成就是这个问题。

模块是单例

NestJS 里的模块都是单例的。什么意思呢?

不管有多少个模块导入了 UsersModuleUsersModule 在整个应用中只有一个实例。同理,它导出的 UsersService 也只有一个实例,所有模块共享同一个。

这带来两个好处:

  1. 省内存——不会重复创建多个实例
  2. 状态一致——所有模块操作的是同一份数据
Tip

这也是为什么你不应该在 Service 里用 private users = [] 这种内存数组来存数据——虽然只有一个实例,但重启就没了。后面会接真正的数据库。

模块重导出

模块可以”转发”别的模块。比如你有一个 SharedModule,把常用的模块打包在一起:

@Module({
  imports: [CommonModule, UtilsModule],
  exports: [CommonModule, UtilsModule],  // 重导出
})
export class SharedModule {}

别的模块只需要导入 SharedModule,就能同时使用 CommonModuleUtilsModule 导出的服务:

@Module({
  imports: [SharedModule],  // 一个 imports 搞定两个
})
export class UsersModule {}

这在大型项目里很实用。把公共依赖打包成一个共享模块,避免到处重复导入。

全局模块

每次用 UsersService 都要导入 UsersModule,写多了有点烦。能不能一次注册,到处使用?

可以,用 @Global() 装饰器:

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

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

全局模块的服务在所有模块中都能直接注入,不需要在 imports 里声明:

@Injectable()
export class UsersService {
  // 不用导入 ConfigModule,直接用
  constructor(private configService: ConfigService) {}
}

全局模块只需要在根模块里注册一次:

@Module({
  imports: [ConfigModule],  // 注册一次,全局可用
})
export class AppModule {}
Tip

全局模块很方便,但别滥用。只有那些真正”到处都要用”的服务才适合做成全局的,比如配置服务、日志服务。如果什么都全局,模块的封装意义就没了。

模块里也能注入服务

模块类本身也能通过构造函数注入服务。这在需要动态配置模块时很有用:

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}
Note

模块类不能把自己注入到自己里面——这会导致循环依赖。模块里注入的服务必须是它自己的 providers 或从 imports 的模块中导出的。

循环依赖怎么办

有时候会出现 A 依赖 B、B 又依赖 A 的情况。比如 UsersModule 需要 AuthServiceAuthModule 需要 UsersService

NestJS 提供了 forwardRef 来解决这个问题:

// users.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';

@Module({
  imports: [forwardRef(() => AuthModule)],
  exports: [UsersService],
})
export class UsersModule {}
// auth.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [forwardRef(() => UsersModule)],
  exports: [AuthService],
})
export class AuthModule {}

服务层面也要用 forwardRef

@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}
Tip

循环依赖通常说明设计有问题。如果两个模块互相依赖,考虑是不是该把它们合并,或者抽出一个公共模块来打破循环。forwardRef 是应急方案,不是长久之计。

模块生命周期

NestJS 的模块和应用都有生命周期钩子,让你在特定时机执行逻辑。

钩子触发时机
onModuleInit()模块初始化完成后
onModuleDestroy()模块销毁前
onApplicationBootstrap()整个应用启动完成后
onApplicationShutdown()应用关闭前

使用方式:

import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Module({})
export class UsersModule implements OnModuleInit, OnModuleDestroy {
  onModuleInit() {
    console.log('UsersModule 初始化完成');
    // 比如:预热缓存、建立数据库连接
  }

  onModuleDestroy() {
    console.log('UsersModule 即将销毁');
    // 比如:清理资源、关闭连接
  }
}
Note

生命周期钩子在 Provider(Service)上也能用,而且用得更多。模块级别的钩子适合做一些模块级别的初始化或清理工作。

模块组织建议

项目大了之后,模块怎么划分是个实际问题。几个原则:

按业务功能划分

src/modules/
├── users/       # 用户模块
├── auth/        # 认证模块
├── products/    # 商品模块
├── orders/      # 订单模块
└── payments/    # 支付模块

公共能力单独放

src/common/
├── database/    # 数据库连接
├── logger/      # 日志服务
├── config/      # 配置服务
└── guards/      # 全局守卫

每个模块职责单一

一个模块只管一件事。UsersModule 只管用户相关的逻辑,AuthModule 只管认证相关的逻辑。不要搞一个”万能模块”什么都往里塞。

小结

模块是 NestJS 的骨架,它决定了你的应用怎么组织。

核心就四件事:

  • imports:我要用别的模块的东西
  • controllers:我有哪些控制器
  • providers:我有哪些服务
  • exports:我有哪些服务可以给别人用

记住几个要点:

  • 模块默认封装,想共享必须 exports
  • 模块是单例,服务也是单例
  • 全局模块方便但别滥用
  • 循环依赖用 forwardRef 应急,但最好重新设计