以 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)来支持更灵活的依赖注入。

相关推荐
irving同学462387 天前
TypeORM 列装饰器完整总结
前端·后端·nestjs
Wang's Blog8 天前
Nestjs框架: 基于策略的权限控制(ACL)与数据权限设计
nestjs·rbac·acl
三十_8 天前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs
SuperheroMan824668 天前
部署时报错:Type 'string' is not assignable to type 'never'(Prisma 关联字段问题)
nestjs
郭俊强10 天前
nestjs 缓存配置及防抖拦截器
缓存·nestjs·防抖
用户8001536355013 天前
在 Nest.js 中实现文件上传
nestjs
三十_14 天前
NestJS 开发必备:HTTP 接口传参的 5 种方式总结与实战
前端·后端·nestjs
关山月17 天前
使用Nest.js设计RBAC权限系统:分步指南
nestjs