3 min read

Nest.js 自动加载模块

Nest.js 拥有一个及其复杂繁琐的模块系统,用来管理应用结构。

每当你在模块内新增一个 Controller、Provider 或是 Module,你都需要显式把 Injectable Class 导入并且传入到相应的字段(@Module { controllers, providers, modules })里。对于一些小应用来说,零星的一些组件显式导入,并不会很麻烦;但是如果是大型应用项目,拥有众多模块,模块内部又有大量的组件时,维护组件模块导入会变得非常麻烦。

我们可以通过 Nest.js 的动态模块功能,实现自动导入指定文件夹下的所有 Nest.js 组件。我在开发的过程中实现了三个有用的动态模块,分别是:AutoControllerModule、AutoProviderModule 和 AutoSequelizeModelModule。这三个动态模块实现方式类似,所以我只对第一个做简要注释说明。

import { DynamicModule, Logger, Module } from "@nestjs/common";
import glob from 'glob';
import { chain } from "lodash";

export interface AutoProviderModuleOptions {
  name?: string;
  global?: boolean;
  path: string[];
}

@Module({})
export class AutoProviderModule {

  static forRoot(data: AutoProviderModuleOptions): DynamicModule {
    const providers: any[] = chain(data.path)
      // 在每一个指定的路径下寻找所有符合条件的文件
      .map((i) => glob.sync(i))
      .flatten()
      // 尝试导入 js 模块
      .map((path) => require(path))
      .map((i: any): any[] => {
        return Object.values(i) as any[]
      })
      .flatten()
      // 对于 Nest.js Provider
      // 导入的值首先得是class函数类型( class Test {}, typeof Test === 'function')
      // class 有 @Injectable 装饰器,具体体现为 Reflect.getOwnMetadataKeys 有 '__injectable__'
      .filter((i) => typeof i === 'function' && Reflect.getOwnMetadataKeys(i).includes('__injectable__'))
      .value();

    // 日志输出有哪些 Provider 被自动识别并导入
    if (providers.length > 0) {
      Logger.log(`${data.name ? `[${data.name}] ` : ''}Auto loaded: ${providers.map((i) => i.name).join(', ')}`, this.name);
    } else {
      Logger.warn(`${data.name ? `[${data.name}] ` : ''}No providers found`, this.name);
    }

    // 创建动态模块
    return {
      module: AutoProviderModule,
      global: data.global,
      providers: [...providers],
      exports: [...providers],
    };
  }
}
AutoProviderModule.ts
import { DynamicModule, Global, Logger, Module } from "@nestjs/common";
import { SequelizeModule } from "@nestjs/sequelize";
import { DEFAULT_CONNECTION_NAME } from "@nestjs/sequelize/dist/sequelize.constants";
import { EntitiesMetadataStorage } from "@nestjs/sequelize/dist/entities-metadata.storage";
import glob from 'glob';
import { chain } from "lodash";
import { Model } from "sequelize-typescript";

export interface AutoSequelizeModelModuleOptions {
  global?: boolean;
  connection?: string;
  path: string[];
}

@Global()
@Module({})
export class AutoSequelizeModelModule {

  static provide = Symbol("AutoSequelizeModel");

  static models(connection: string = DEFAULT_CONNECTION_NAME): Function[] {
    return EntitiesMetadataStorage.getEntitiesByConnection(connection)
  }

  static forRoot(data: AutoSequelizeModelModuleOptions): DynamicModule {
    const connection = data?.connection || DEFAULT_CONNECTION_NAME;

    const models: any[] = chain(data.path)
      .map((i) => glob.sync(i))
      .flatten()
      .map((path) => require(path))
      .map((i: any): any[] => {
        return Object.values(i) as any[]
      })
      .flatten()
      .filter((i) => typeof i === 'function' && Model.isPrototypeOf(i))
      .value();

    const feature = SequelizeModule.forFeature(models, connection)
    if (models.length > 0) {
      Logger.log(`Auto loaded: ${models.map((i) => i.name).join(', ')}`, this.name);
    } else {
      Logger.warn(`No models found`, this.name);
    }

    const provider = {
      provide: AutoSequelizeModelModule.provide,
      useValue: models,
    }

    return {
      module: AutoSequelizeModelModule,
      global: data.global,
      imports: [feature],
      providers: [provider],
      exports: [provider, feature]
    };
  }
}
AutoSequelizeModelModule.ts
import { DynamicModule, Logger, Module } from "@nestjs/common";
import glob from 'glob';
import { chain } from "lodash";

export interface AutoControllerModuleOptions {
  name?: string;
  global?: boolean;
  path: string[];
}

@Module({})
export class AutoControllerModule {

  static forRoot(data: AutoControllerModuleOptions): DynamicModule {
    const controllers: any[] = chain(data.path)
      .map((i) => glob.sync(i))
      .flatten()
      .map((path) => require(path))
      .map((i: any): any[] => {
        return Object.values(i) as any[]
      })
      .flatten()
      .filter((i) => typeof i === 'function' && Reflect.getOwnMetadataKeys(i).includes('__controller__'))
      .value();

    if (controllers.length > 0) {
      Logger.log(`${data.name ? `[${data.name}] ` : ''}Auto loaded: ${controllers.map((i) => i.name).join(', ')}`, this.name);
    } else {
      Logger.warn(`${data.name ? `[${data.name}] ` : ''}No controllers found`, this.name);
    }

    return {
      module: AutoControllerModule,
      global: data.global,
      providers: [...controllers],
      controllers: [...controllers],
      exports: [...controllers],
    };
  }
}
AutoControllerModule.ts

使用示例:

const autoProviderModule = AutoProviderModule.forRoot({
  path: [
    path.join(__dirname, "./services/**/*.js"),
  ],
})

@Module({
  imports: [
    autoProviderModule,
  ],
  exports: [
    autoProviderModule,
  ],
})
export class AppModule {}