以 NestJS 为原型看懂 Node.js 框架设计:Module

前言

在上一章中,我们实现了一个简化版的依赖注入(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);

模块化可以带来:

  1. providerscontrollersModule划分,职责分明,作用域清晰。
  2. Module 之间相互独立,但可以通过配置exports暴露相关ProviderModule 供外部使用

如何复用模块?

模块之间是相互独立的,而 exports 是模块之间的桥梁,它有两种用法:

  1. 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 {}
  1. 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,过程如下:

  1. 通过instanceWrapper.metatype,生成构造函数参数类型列表(token list)
  2. 根据参数类型(token)找到对应provider,必要时递归imports查找
  3. 若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 回调中执行实例的相关方法。

参考源码:PathsExplorer.scanForPaths

基于之前的实现,这里不再按需解析模块,而是直接获取上一步生成的的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;
    // ...
}

小结

到目前为止,我们可以得知:

  1. module 之间相互独立,provider 的复用是通过 exports 来实现的。
  2. 默认情况下,module、controller、provider 都是单例 Singleton(只实例化一次)。可以通过设置不同injectionToken 使 provider 实例化多次。

总结

目前我们在应用初始化时就完成了所有 provider 与 controller 的实例化,并将实例对象存放在对应的 InstanceWrapper.instance 中。这与 Nest 官方的默认策略一致 ------ 单例(Singleton)在启动阶段会被统一预实例化

但由于单例在实例化时还没有请求上下文(如 req 对象),因此无法在构造函数里直接注入与请求相关的依赖。为了解决这类问题,下一章我们将引入 provider 的作用域(Scope) ,通过调整实例化的时机(如 Request / Transient)来支持更灵活的依赖注入。

相关推荐
Eric_见嘉2 天前
NestJS 🧑‍🍳 厨子必修课(九):API 文档 Swagger
前端·后端·nestjs
XiaoYu200210 天前
第3章 Nest.js拦截器
前端·ai编程·nestjs
XiaoYu200211 天前
第2章 Nest.js入门
前端·ai编程·nestjs
实习生小黄12 天前
NestJS 调试方案
后端·nestjs
当时只道寻常15 天前
NestJS 如何配置环境变量
nestjs
濮水大叔1 个月前
VonaJS是如何做到文件级别精确HMR(热更新)的?
typescript·node.js·nestjs
ovensi1 个月前
告别笨重的 ELK,拥抱轻量级 PLG:NestJS 日志监控实战指南
nestjs
ovensi1 个月前
Docker+NestJS+ELK:从零搭建全链路日志监控系统
后端·nestjs
Gogo8161 个月前
nestjs 的项目启动
nestjs