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