说到“模块”两字,我们脑海里肯定会浮现很多关于它好处的词汇:封装性、可复用、按需引入等等。当一个软件系统的代码规模上升到一定复杂度后,我们的确需要一些方式来条理更清晰的组织我们的代码,让代码更易阅读、团队分工协作更方便。
从一开始没有模块系统,到之后出现几大类(AMD、CMD、CommonJS、ESM)下的多种模块系统,JavaScript的代码组织和管理变得渐渐规范起来。我们可以统称这些模块系统为JavaScript模块系统,它实现了从文件层面上对变量、函数、类等各种JS内容的隔离封装,为这些内容划出了边界,并开放有限可互相沟通的入口。
NestJS框架中,在使用了JavaScript模块系统的基础上,又引入了一种特有的模块系统,就称呼它为NestJS模块系统吧,它只用于管理NestJS应用程序中的特定资源内容,声明它们在依赖注入环境下的作用域。
从之前介绍依赖注入的文章中,我们知道了NestJS中存在容器这样一个东西,那现在请把容器想象成一个集装箱,而放在这个集装箱中的一个个打包好的快递包裹就是NestJS模块,并且每个包裹里的内容只限于NestJS模块允许打包进去的东西:控制器、资源提供者。
每个NestJS应用程序其实是由模块组合而成的,它至少需要有一个模块(称为根模块)。多个模块组成一个树状结构。小型应用可能只需要一个根模块就行了,大型应用通常会由大量模块组织而成。
模块的创建
NestJS模块可以通过在一个普通的类上添加@Modue装饰器声明来创建。
代码语言:javascript复制import { Module } from "@nestjs/common";
@Module({
imports: [],
controllers: [],
providers: [],
exports: [],
})
export class DemoModule { }
@Module装饰器有4个配置项,它们的作用分别如下:
- imports - 需要导入当前模块的其他模块
- providers - 属于当前模块的资源提供者
- controllers - 属于当前模块的路由控制器
- exports - 当其他模块导入当前模块后,可访问到的属于当前模块的资源提供者、或由当前模块导入的其他模块
值得记住的一点是:模块默认情况对外界访问是封闭的。也就是说,一个模块在未作特别声明的情况下,其内部的资源是不能在两个模块间进行互相依赖注入的,只有本模块内部的资源才能互相注入。如果要支持跨模块注入,则需要使用上面的exports选项进行声明:
代码语言:javascript复制import { Module } from "@nestjs/common";
import { DemoService } from "./demo.service";
@Module({
imports: [],
controllers: [],
providers: [DemoService],
exports: [DemoService],
})
export class DemoModule { }
模块的分类:功能模块与共享模块
在实际的软件程序中,一定会存在业务类代码和辅助工具类代码。有了模块系统,我们能更好的归类划分不同职责的代码。划分的原则还是以业务和非业务功能为基础,业务上相关联的代码(包括只在该业务中所使用的工具代码)尽量组织在同一个模块中;而和业务无关的、可被其他模块通用的代码,可以按功能分类组织在一个或多个模块之中。
模块的重组
一个模块可以通过imports导入其他模块,也可以通过exports再次导出这些导入的模块。这样做的目的是:可以实现将各种小粒度的模块排列组合成各种稍大粒度的模块,按照实际需要选择使用稍大粒度的模块,而不是总导入数量较多的小粒度模块。
代码语言:javascript复制@Module({
imports: [HelperAModule, HelperBModule],
exports: [HelperAModule, HelperBModule],
})
export class HelperModule {}
模块的依赖注入
模块类本身也可以进行依赖注入,让其他资源注入到模块类中。如下所示:
代码语言:javascript复制import { Module } from '@nestjs/common';
import { DemoService } from './demo.service';
@Module({
imports: [],
controllers: [],
providers: [DemoService],
exports: [DemoService],
})
export class DemoModule {
constructor(private readonly demoService: DemoService) {
console.log(demoService);
}
}
模块的全局化
假设你有一些模块(比如数据库连接模块、Redis缓存模块、一些公用工具模块等),它们几乎在你所有的其他模块中都会被用到,那么你需要在所有这些用到它们的模块中都导入它们,这会让你的代码看起来有那么点啰嗦。
为了解决这个问题,NestJS提供了将模块声明成全局作用域的方式,即使用@Global装饰器:
代码语言:javascript复制import { Module, Global } from '@nestjs/common';
import { DemoService } from './demo.service';
@Global()
@Module({
imports: [],
controllers: [],
providers: [DemoService],
exports: [DemoService],
})
export class DemoModule {}
这样一来,需要使用到这个DemoModule中资源的其他模块,就不需要通过imports来导入它就能使用了。
动态模块
有时候,为了一个模块更好的被复用,我们希望它可以通过配置参数的形式来提供具有差异化的功能。比如一个数据库连接模块,你肯定不希望它总是连接的同一个服务器上的数据库,或者用户名和密码总是固定的。所以,像这样的模块,我们希望它实例化的时候是可接受额外参数,或者可以自定义一些中间过程。为了实现这样的功能,NestJS模块提供了可动态生成模块实例的方式,来看下面的示例,它将通过一个参数来让模块中的资源提供者产生变化:
代码语言:javascript复制import { Module, DynamicModule } from '@nestjs/common';
import { DemoService } from './demo.service';
@Module({})
export class DemoModule {
static register(options): DynamicModule {
// Mockup对象
const mockDemoService = {
test() {
return 'hello,world';
}
};
const definition = {
module: DemoModule,
imports: [],
controllers: [],
providers: [
// 根据配置参数中的isDebug值,来决定使用真正的DemoService
// 作为资源提供者,还是用mockup对象
options.isDebug ? {
provide: DemoService,
useValue: mockDemoService
} : DemoService
],
exports: [DemoService],
};
return definition;
}
}
我们将本来模块类上的@Module装饰器的参数选项都移除,然后在DemoModule模块类中定义一个静态方法register,该方法接受一个options参数(其实这里的方法名和参数名、参数个数都可以随你自己的需要来定,没有什么限制),且该方法返回的类型为DynamicModule。然后该方法内部就是具体去拼装一个和@Module装饰器参数选项类似的动态模块信息了。
实现上述的动态模块后,在使用它的地方就可以这样来写:
代码语言:javascript复制import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DemoModule } from './demo.module';
@Module({
// 调用模块中的静态方法获取动态模块
imports: [DemoModule.register({ isDebug: false })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule { }
是不是非常容易理解?
总结
使用好NestJS的模块系统,并结合依赖注入,可以更好的去管理你的应用程序代码。在设计系统时,请一定要事先规划一下你的模块,以及互相间的依赖关系,可以让你在开发实现时事半功倍。