Node.js服務端開發教程 (七):模組系統

  • 2019 年 11 月 29 日
  • 筆記

說到「模組」兩字,我們腦海里肯定會浮現很多關於它好處的辭彙:封裝性、可復用、按需引入等等。當一個軟體系統的程式碼規模上升到一定複雜度後,我們的確需要一些方式來條理更清晰的組織我們的程式碼,讓程式碼更易閱讀、團隊分工協作更方便。

從一開始沒有模組系統,到之後出現幾大類(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的模組系統,並結合依賴注入,可以更好的去管理你的應用程式程式碼。在設計系統時,請一定要事先規劃一下你的模組,以及互相間的依賴關係,可以讓你在開發實現時事半功倍。