Nestjs 动态模块

227 阅读3分钟

动态模块

使用模块是构建高效、可维护和可扩展的应用程序的关键所在。模块使得开发者能够清晰地分离关注点,将应用程序的不同部分封装到独立的模块中。

以日志模块为例,展示如何创建一个标准的 NestJS 动态模块,以下是详细的步骤指南:

目录结构

首先,你需要为日志模块创建一个基本的文件结构。这通常包括一个模块文件(如 logger.module.ts)、一个服务文件(如 logger.service.ts),以及可能需要的接口(interfaces)和适配器(adapters)。

├── src
│	├── configs
│	│   ├── logger-config.ts
│	│   └── index.ts
│	├── logger
│	│	├── adapters
│	│   │	├── winston-adapter.ts
│	│   │	└── index.ts
│	│	├── interfaces
│	│   │	├── logger.interface.ts
│	│   │	├── logger-module-options.ts
│	│   │	└── index.ts
│   │   ├── logger.module-definition.ts
│   │   ├── logger.service.ts
│   │   ├── logger.module.ts
│   │   └── index.ts
│	├── app.controller.ts
│	├── app.service.ts
│   ├── app.module.ts
│	└── main.ts
└── tsconfig.json

定义接口

定义服务需要实现的 ILogger 接口,这里直接继承自 Nest 内部提供的 LoggerService 接口。

import { LoggerService } from "@nestjs/common";

export interface ILogger extends LoggerService {}

logger.service.ts 文件中,通过模块配置将具体的实现类传递给服务。

import { Inject, Injectable } from "@nestjs/common";
import { ILogger, LoggerModuleOptions } from "./interfaces";
import { MODULE_OPTIONS_TOKEN } from "./logger.module-definition";

@Injectable()
export class LoggerService implements ILogger {
	protected readonly logger: ILogger;
	constructor(
		@Inject(MODULE_OPTIONS_TOKEN)
		private readonly options: LoggerModuleOptions,
	) {
		if (!this.options.logger) {
			throw new Error("LoggerModuleOptions.logger is not defined");
		}
		this.logger = this.options.logger;
	}

	log(message: any, ...optionalParams: any[]) {
		this.logger.log(message, ...optionalParams);
	}

	error(message: any, ...optionalParams: any[]) {
		this.logger.error(message, ...optionalParams);
	}

	warn(message: any, ...optionalParams: any[]) {
		this.logger.warn(message, ...optionalParams);
	}
}

实现服务

WinstonAdapter 使用 winston 实现实际的日志记录功能,比如将日志信息输出到控制台、文件或其他日志存储系统。

import { inspect } from "util";
import * as winston from "winston";
import { ILogger } from "../../interfaces";

export class WinstonAdapter implements ILogger {
	protected readonly logger: winston.Logger;

	constructor(private readonly options: winston.LoggerOptions) {
		this.logger = winston.createLogger(this.options);
	}

	log(message: any, ...optionalParams: any[]) {
		this.logger.info(this.format(message, ...optionalParams));
	}

	error(message: any, ...optionalParams: any[]) {
		this.logger.error(this.format(message, ...optionalParams));
	}

	warn(message: any, ...optionalParams: any[]) {
		this.logger.warn(this.format(message, ...optionalParams));
	}

	private format(...messages: unknown[]) {
		return messages
			.map((m) => (typeof m === "string" ? m : inspect(m)))
			.join(" ");
	}
}

构建模块

NestJS 提供了一个 ConfigurableModuleBuilder 类用于便捷的构建动态模块的配置。

import { ConfigurableModuleBuilder } from "@nestjs/common";
import { LoggerModuleOptions } from "./interfaces";

export const {
	ConfigurableModuleClass,
	MODULE_OPTIONS_TOKEN,
	OPTIONS_TYPE,
	ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<LoggerModuleOptions>()
	.setExtras(
		{
			isGlobal: true,
		},
		(definition, extras) => ({
			...definition,
			global: extras.isGlobal,
		}),
	)
	.build();

使用 @Module 装饰器来定义日志模块。这个模块将包含日志服务作为提供者,并可以配置为动态模块,以便在应用程序的不同部分中按需加载。

import { DynamicModule, Module } from "@nestjs/common";
import {
	ASYNC_OPTIONS_TYPE,
	ConfigurableModuleClass,
	OPTIONS_TYPE,
} from "./logger.module-definition";
import { LoggerService } from "./logger.service";

@Module({
	providers: [LoggerService],
	exports: [LoggerService],
})
export class LoggerModule extends ConfigurableModuleClass {
	static register(options: typeof OPTIONS_TYPE): DynamicModule {
		return {
			...super.register(options),
		};
	}

	static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
		return {
			...super.registerAsync(options),
		};
	}
}

注册模块

由于它是一个动态模块,你可以使用 LoggerModule.register()LoggerModule.registerAsync() 方法在根模块或其他模块中导入它。

import path from "path";
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { LoggerModule } from "@/logger";
import { configs } from "./configs";

@Module({
	imports: [
		ConfigModule.forRoot({
			isGlobal: true,
			cache: true,
			envFilePath: [
				path.resolve(
					__dirname,
					"..",
					`env/${process.env.NODE_ENV || "development"}.env`,
				),
			],
			load: configs,
		}),
		LoggerModule.registerAsync({
			inject: [ConfigService],
			useFactory: (configService: ConfigService) => ({
				logger: new WinstonAdapter(configService.get("logger")),
			}),
		}),
		...
	],
	providers: [
		...
	],
})
export class AppModule {}

使用服务

最后,你可以在需要记录日志的地方注入 LoggerService 并使用它的方法。这可以通过构造函数注入或其他依赖注入机制来实现。 在这个日志模块中,我们通过适配器模式实现了对Winston日志库的封装,使得日志服务能够灵活地切换到不同的日志框架,而无需修改服务调用方的代码。

import { Injectable } from "@nestjs/common";
import { LoggerService } from "@/logger";

@Injectable()
export class AppService {
	constructor(
		private readonly loggerService: LoggerService,
	) {
		loggerService.log("Hello!");
	}
}

原文: hikestack.github.io/official/gu…