前言
在上一章中,我们实现了一个简化版的依赖注入(DI)容器,能够自动化地创建并管理依赖。
这解决了组件之间的耦合问题,但还留有一个更大的挑战:如何在应用规模变大时,保持清晰的结构与边界?
本章我们将聚焦在 Module 的设计与实现。通过模块化,我们可以把 Controller 和 Provider 按照业务领域划分,形成一个个独立的单元,并通过 imports 和 exports 来组织模块之间的依赖关系。这也是从「DI 容器」迈向「模块化应用」的关键一步。
为什么需要模块化?
在实际项目中,当 Controller / Service 越来越多时,手动注册会非常痛苦,比如下面这样:
ts
const app = createApp();
app.registerController(UserController);
app.registerController(PostController);
app.registerProvider(UserService);
app.registerProvider(PostService);
当我们将项目按照业务划分成不同的模块,用 @Module 后,可以:
ts
// config.module.ts
@Module({
providers: [ConfigService],
exports: [ConfigService]
})
export class ConfigModule {}
// user.module.ts
@Module({
imports: [ConfigModule],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
// app.module.ts
@Module({
imports: [UserModule, xxxModule]
})
export class AppModule {}
// main.ts
const app = createApp(AppModule);
模块化可以带来:
providers
,controllers
按Module
划分,职责分明,作用域清晰。Module
之间相互独立,但可以通过配置exports
暴露相关Provider
或Module
供外部使用
如何复用模块?
模块之间是相互独立的,而 exports 是模块之间的桥梁,它有两种用法:
- export provider:供直接上级使用,此时 BModule 内部可注入AService。(只能export 自身的provider)
ts
@Module({
providers: [AService],
exports: [AService]
})
export class AModule {}
@Module({
imports: [AModule],
controllers: [BController],
providers: [BService],
})
export class BModule {}
- export module:不仅自己用,上级也要用, 此时BModule,CModule 内部皆可注入AService。(只能export 自身import的module)
ts
@Module({
providers: [AService],
exports: [AService]
})
export class AModule {}
@Module({
imports: [AModule],
controllers: [BController],
providers: [BService],
exports: [AModule]
})
export class BModule {}
@Module({
imports: [BModule],
controllers: [CController],
providers: [CService],
})
export class CModule {}
Warning: 根据源码,一个模块只能导出 provider 或 imports 中的 配置项,否则会报错噢。
ts
// Nest: packages/core/injector/module.ts
class Module {
public validateExportedProvider(token: InjectionToken) {
if (this._providers.has(token)) {
return token;
}
const imports = iterate(this._imports.values())
.filter(item => !!item)
.map(({ metatype }) => metatype)
.filter(metatype => !!metatype)
.toArray();
if (!imports.includes(token as Type<unknown>)) {
const { name } = this.metatype;
const providerName = isFunction(token) ? (token as Function).name : token;
throw new UnknownExportException(providerName as string, name);
}
return token;
}
}
功能实现
在实现Module和exports 功能之前,有两个概念需要达成共识。
准备一:Module 定义
根据当前的需求,我们可以定义Module对象如下:
ts
export type InjectionToken<T = any> =
| string
| symbol
| Type<T> // 类
| Abstract<T> // 抽象类
| Function;
export interface InstanceWrapper<T = any> {
token: InjectionToken; // 普通provider或controller时: token === metatype;自定义provider时,token === typeof provider.provide
metatype: Type<T> | Function; // 通过该属性进行实例化; inject 存在时, metatype为普通函数,否则为class。
instance: T | null; // singleton 实例
inject?: InjectionToken[]; // 只有 factory provider 和 useExisting provider 才有
}
export class Module {
private _providers = new Map<InjectionToken, InstanceWrapper>();
private _controllers = new Map<Type, InstanceWrapper>();
private _exports = new Set<InjectionToken>();
private _imports = new Set<Module>();
constructor(public metatype: Type<any>) {}
public addProvider(){}
public addController(){}
public addExportedProviderOrModule() {}
public addImport(){}
}
Modules 是实现控制反转的核心,因此在 DI container 中定义modules如下:
ts
export class Container {
private modules = new Map<Type, Module>();
addModule(moduleClass: Type): Module {
if (!this.modules.has(moduleClass)) {
this.modules.set(moduleClass, new Module(moduleClass));
}
return this.modules.get(moduleClass)!;
}
getModules() {
return this.modules;
}
}
准备二:Module 装饰器
通过装饰器可以拿到用户配置的 providers / controllers / imports / exports,将这几个属性保存至元数据:
ts
interface Type<T = any> extends Function {
new (...args: any[]): T;
}
type Provider<T = any> =
| Type<any>
| ClassProvider<T>
| ValueProvider<T>
| FactoryProvider<T>
| ExistingProvider<T>;
interface ModuleMetadata {
controllers?: Type<any>[];
providers?: Provider[];
imports?: Type<any>[];
exports?: Type<any>[];
}
export function Module(metadata: ModuleMetadata): ClassDecorator {
return (target: Function) => {
for (const property in metadata) {
if (Object.hasOwnProperty.call(metadata, property)) {
Reflect.defineMetadata(property, (metadata as any)[property], target);
// 后续通过Reflect.getMetadata("controllers" | "providers" | "imports" | "exports", target) 拿到对应的数据
}
}
};
}
现在已经有了数据源以及基本数据结构,接下来就是解析和实例化,并实现exports的功能。
解析与实例化(相对源码有所简化)
从NestFactory.create 开始,流程分为4步:
ts
// Nest 推崇单例模式,原则上只会实例化一次,因此源码中多次遍历modules并不会太耗性能。
export class NestFactory {
static async create(moduleCls: IEntryNestModule) {
const httpServer = createHttpServer();
// 创建DI容器
const container = new Container();
// 1. 递归扫描模块(构建模块依赖图);
// 参考源码:dependenciesScanner.scanForModules
this.scanModules(moduleCls, container);
// 2. 注册模块的 providers/controllers/exports
// 参考源码:dependenciesScanner.scanModulesForDependencies
this.registerModules(container);
// 3. 实例化所有依赖
// 参考源码:instanceLoader.createInstances
await container.createInstancesOfDependencies();
// 4. 注册路由(入口模块)
const routerExplorer = new RouterExplorer(httpServer, container);
routerExplorer.registerAllRoutes();
return httpServer;
}
}
Step1:递归扫描模块,构建模块依赖关系(仅添加模块)
ts
export class NestFactory {
private static scanModules(moduleCls: Type<any>, container: Container) {
container.addModule(moduleCls);
const imports = Reflect.getMetadata("imports", moduleCls) || [];
for (const importedModule of imports) {
this.scanModules(importedModule, container);
}
}
}
Step2:遍历所有模块,注册 imports/providers/controllers/exports 到 Module 实例
这里要结合上面的Module定义理解:
ts
export class NestFactory {
private static registerModules(container: Container) {
const modules = container.getModules();
modules.forEach((moduleRef) => {
const moduleClass = moduleRef.metatype;
const imports = Reflect.getMetadata("imports", moduleClass) || [];
for (const c of imports) {
const importedModule = container.getModule(c);
importedModule && moduleRef.addImport(importedModule);
}
const providers = Reflect.getMetadata("providers", moduleClass) || [];
for (const p of providers) {
moduleRef.addProvider(p); // 内部会针对不同类型的provider单独处理
}
const controllers = Reflect.getMetadata("controllers", moduleClass) || [];
for (const c of controllers) {
moduleRef.addController(c);
}
const exportsList = Reflect.getMetadata("exports", moduleClass) || [];
for (const e of exportsList) {
moduleRef.addExportedProviderOrModule(e); // 内部对exports进行合法校验
}
});
}
}
这一步完成后,modules 结构长这样:
ts
Map(3) {
[class AppModule] => Module {
metatype: [class AppModule],
_providers: Map(0) {},
_controllers: Map(1) { [class AppController] => [InstanceWrapper] },
_exports: Set(0) {},
_imports: Set(1) { [Module] }
},
[class UserModule] => Module {
metatype: [class UserModule],
_providers: Map(1) { 'customUserService' => [InstanceWrapper] },
_controllers: Map(1) { [class UserController] => [InstanceWrapper] },
_exports: Set(0) {},
_imports: Set(1) { [Module] }
},
[class LoggerModule] => Module {
metatype: [class LoggerModule],
_providers: Map(1) { [class LoggerService] => [InstanceWrapper] },
_controllers: Map(0) {},
_exports: Set(1) { [class LoggerService] },
_imports: Set(0) {}
}
}
Step3:实例化所有依赖(provider + controller)
这一步通过createInstancesOfDependencies
扫描所有 Module,依次初始化 provider 和 controller。并且赋值给InstanceWrapper.instance
,过程如下:
- 通过instanceWrapper.metatype,生成构造函数参数类型列表(token list)
- 根据参数类型(token)找到对应provider,必要时递归imports查找
- 若provider没有依赖,直接实例化;若有依赖,重复1,2
递归imports查找provider,源码位于 Injector.lookupComponentInImports

ts
export class Container {
private resolve<T>(token: InjectionToken, moduleRef: Module): T {}
// 参考源码:createInstances in packages/core/injector/instance-loader.ts
public async createInstancesOfDependencies() {
for (const module of this.modules.values()) {
for (const instanceWrapper of module.providers.values()) {
this.loadProvider(instanceWrapper, module);
}
for (const instanceWrapper of module.controllers.values()) {
this.loadProvider(instanceWrapper, module);
}
}
}
}
除了构造函数注入依赖,我们还可以通过绑定属性的方式来注入依赖,使用方法如下:
ts
@Injectable()
export class UserService {
@Inject(LoggerService)
private loggerService!: LoggerService;
constructor() {}
}
那么实现起来也很简单,只要在实例化后手动绑定对应的provider即可:
ts
//实现:applyProperties
export class Container {
private loadProvider(wrapper: InstanceWrapper, moduleRef: Module) {
if (wrapper.instance) {
return;
}
if (
wrapper.metatype &&
typeof wrapper.metatype === "function" &&
Array.isArray(wrapper.inject)
) {
this.instantiateFactoryAndExistingProvider(wrapper, moduleRef);
} else if (wrapper.metatype && typeof wrapper.metatype === "function") {
const instance = this.instantiateProvider(wrapper, moduleRef);
this.applyProperties(instance, wrapper.metatype as Type, moduleRef);
}
}
/**
* 注入属性依赖
*/
private applyProperties(instance: any, metatype: Type, moduleRef: Module) {
const properties: Array<{ key: string; type: InjectionToken }> =
Reflect.getMetadata(PROPERTY_DEPS_METADATA, metatype) || [];
for (const { key, type: token } of properties) {
const resolved = this.resolveSingleParam(token, moduleRef);
instance[key] = resolved;
}
}
}
这种属性注入依赖的场景更多是用于 循环依赖 和 可选依赖,避免在构造函数注入该依赖时阻塞。
Step4:注册路由(入口模块)
原理:遍历Object.getOwnPropertyNames(controller.prototype)
,依靠元数据生成路由,在request 回调中执行实例的相关方法。
基于之前的实现,这里不再按需解析模块,而是直接获取上一步生成的的instance,其他逻辑不变。如有兴趣,可点击这里查看相关实现。
ts
// before
export class RouterExplorer {
private registerRoutes(controllerClass: Type<any>, moduleRef: Module) {
const instance: any = this.container.resolve(controllerClass, moduleRef);
// ...
}
ts
// after
export class RouterExplorer {
private registerRoutes(controllerClass: Type<any>, moduleRef: Module) {
const wrapper = moduleRef.controllers.get(controllerClass);
if (!wrapper || !wrapper.instance) return;
const instance = wrapper.instance;
// ...
}
小结
到目前为止,我们可以得知:
- module 之间相互独立,provider 的复用是通过 exports 来实现的。
- 默认情况下,module、controller、provider 都是单例 Singleton(只实例化一次)。可以通过设置不同
injectionToken
使 provider 实例化多次。
总结
目前我们在应用初始化时就完成了所有 provider 与 controller 的实例化,并将实例对象存放在对应的 InstanceWrapper.instance
中。这与 Nest 官方的默认策略一致 ------ 单例(Singleton)在启动阶段会被统一预实例化。
但由于单例在实例化时还没有请求上下文(如 req 对象),因此无法在构造函数里直接注入与请求相关的依赖。为了解决这类问题,下一章我们将引入 provider 的作用域(Scope) ,通过调整实例化的时机(如 Request / Transient)来支持更灵活的依赖注入。