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],
};
}
}
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]
};
}
}
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],
};
}
}
使用示例:
const autoProviderModule = AutoProviderModule.forRoot({
path: [
path.join(__dirname, "./services/**/*.js"),
],
})
@Module({
imports: [
autoProviderModule,
],
exports: [
autoProviderModule,
],
})
export class AppModule {}