Node.js服务端开发教程 (七):模块系统

最新推荐:《Vue3.0抢先学》系列学习教程

说到“模块”两字,我们脑海里肯定会浮现很多关于它好处的词汇:封装性、可复用、按需引入等等。当一个软件系统的代码规模上升到一定复杂度后,我们的确需要一些方式来条理更清晰的组织我们的代码,让代码更易阅读、团队分工协作更方便。

从一开始没有模块系统,到之后出现几大类(AMD、CMD、CommonJS、ESM)下的多种模块系统,JavaScript的代码组织和管理变得渐渐规范起来。我们可以统称这些模块系统为JavaScript模块系统,它实现了从文件层面上对变量、函数、类等各种JS内容的隔离封装,为这些内容划出了边界,并开放有限可互相沟通的入口。

NestJS框架中,在使用了JavaScript模块系统的基础上,又引入了一种特有的模块系统,就称呼它为NestJS模块系统吧,它只用于管理NestJS应用程序中的特定资源内容,声明它们在依赖注入环境下的作用域。

从之前介绍依赖注入的文章中,我们知道了NestJS中存在容器这样一个东西,那现在请把容器想象成一个集装箱,而放在这个集装箱中的一个个打包好的快递包裹就是NestJS模块,并且每个包裹里的内容只限于NestJS模块允许打包进去的东西:控制器、资源提供者。

每个NestJS应用程序其实是由模块组合而成的,它至少需要有一个模块(称为根模块)。多个模块组成一个树状结构。小型应用可能只需要一个根模块就行了,大型应用通常会由大量模块组织而成。

模块的创建

NestJS模块可以通过在一个普通的类上添加@Modue装饰器声明来创建。

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

@Module({
    imports: [],
    controllers: [],
    providers: [],
    exports: [],
})
export class DemoModule { }

@Module装饰器有4个配置项,它们的作用分别如下:

  • imports - 需要导入当前模块的其他模块
  • providers - 属于当前模块的资源提供者
  • controllers - 属于当前模块的路由控制器
  • exports - 当其他模块导入当前模块后,可访问到的属于当前模块的资源提供者、或由当前模块导入的其他模块

值得记住的一点是:模块默认情况对外界访问是封闭的。也就是说,一个模块在未作特别声明的情况下,其内部的资源是不能在两个模块间进行互相依赖注入的,只有本模块内部的资源才能互相注入。如果要支持跨模块注入,则需要使用上面的exports选项进行声明:

import { Module } from "@nestjs/common";
import { DemoService } from "./demo.service";

@Module({
    imports: [],
    controllers: [],
    providers: [DemoService],
    exports: [DemoService],
})
export class DemoModule { }

模块的分类:功能模块与共享模块

在实际的软件程序中,一定会存在业务类代码和辅助工具类代码。有了模块系统,我们能更好的归类划分不同职责的代码。划分的原则还是以业务和非业务功能为基础,业务上相关联的代码(包括只在该业务中所使用的工具代码)尽量组织在同一个模块中;而和业务无关的、可被其他模块通用的代码,可以按功能分类组织在一个或多个模块之中。

模块的重组

一个模块可以通过imports导入其他模块,也可以通过exports再次导出这些导入的模块。这样做的目的是:可以实现将各种小粒度的模块排列组合成各种稍大粒度的模块,按照实际需要选择使用稍大粒度的模块,而不是总导入数量较多的小粒度模块。

@Module({
  imports: [HelperAModule, HelperBModule],
  exports: [HelperAModule, HelperBModule],
})
export class HelperModule {}

模块的依赖注入

模块类本身也可以进行依赖注入,让其他资源注入到模块类中。如下所示:

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装饰器:

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模块提供了可动态生成模块实例的方式,来看下面的示例,它将通过一个参数来让模块中的资源提供者产生变化:

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装饰器参数选项类似的动态模块信息了。

实现上述的动态模块后,在使用它的地方就可以这样来写:

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的模块系统,并结合依赖注入,可以更好的去管理你的应用程序代码。在设计系统时,请一定要事先规划一下你的模块,以及互相间的依赖关系,可以让你在开发实现时事半功倍。

关注首发公众号:默碟

推荐阅读更多精彩内容