Angular 17+ 高級教程 – NgModule

前言

NgModule 在 Angular v14 以前是一門必修課。然而,自 Angular v14 推出 Standalone Component 以後,它的地位變得越來越邊緣化。

本教程從開篇到本篇,所有例子使用的都是 Standalone Component,一點 NgModule 的影子也沒有😔。

但是!NgModule 還是有它的價值的,而且在越複雜的項目中你越可以體會到它的價值。

本篇,就讓我們一起學習這個被遺忘了但其實很強大的 NgModule 吧🚀。

 

NgModule 有啥用?

NgModule 主要是用於 (組件 / 指令 / Pipe) 的管理。

是的,你沒聽錯,就是管理。

一個項目即使完全不使用 NgModule 也不會有什麼功能做不出來。

用與不用只在管理上有區別,在功能上是沒有區別的。

這也是爲什麼 NgModule 會越來越不被重視,畢竟項目只有複雜到一定程度才需要管理的丫。

 

組件 / 指令 / Pipe の 管理

Standalone Component 對組件 / 指令 / Pipe (簡稱組件) 的管理方式是這樣的:

  1. 當一個組件想使用另一個組件時,就 import 它。
  2. 每一個組件都是公開的,意思就是任何一個組件都可以 import 任何一個組件。

Standalone Component 的管理方式簡單明瞭,但在複雜項目中經常會遇到二個煩人問題。

A lot of component imports

比如說,我們有一個封裝好的 table 組件,長這樣

它不是簡簡單單的一個組件,要使用它還需要搭配一些相關的指令。

每一次要使用這個 table 組件,就需要 import 一個組件 +  5 個指令,總共 6 個 importable。

這 6 個 importable 是有密切關係的,但是從 imports 代碼上完全看不出來。

有關聯的東西,卻沒有被 group 在一起,這在管理上就是扣分的。

或許你會想,爲什麼一個 table 組件需要配那麼多指令,不可以一個 table 組件完事嗎?

理論上是可以,但是管理上不行,因爲 table 是一個複雜的組件,如果把所有邏輯都塞在一個地方,管理就會很亂。

軟件工程的奧義就是把大的邏輯拆小,通過組合和關聯把小的邏輯組裝大。這樣只要確保小的可運行,慢慢推導到大的也可以運行,軟件就穩定了。

再看一個例子:

select 和 table 都遇到了相同的問題。

Private 組件

Standalone Component 的第二條規則:

每一個組件都是公開的,意思就是任何一個組件都可以 import 任何一個組件

表面上,select 組件是由 select, option, optgroup 組件組裝而成。但或許 select 組件內還使用了其它的組件,只是對於使用者來說它被封裝起來了而且。

如果說 select 組件內部所使用的組件也是公開的 (因爲 Standalone 組件一定是公開的),這樣就會對使用者造成一些混亂,更正確的做法是引入 private 概念。

也就是說,某些組件應該只能被某些組件使用,而不是所有組件都可以使用。

 

使用 NgModule 管理組件 / 指令 / Pipe

創建一個項目

ng new module --routing=false --ssr=false --skip-tests --style=scss

Create NgModule

創建一個 NgModule 和一些組件

ng generate module dialog;
ng generate component dialog/dialog --standalone=false --module=dialog;
ng generate component dialog/dialog-public --standalone=false --module=dialog;
ng generate component dialog/dialog-private --standalone=false --module=dialog;

shortform 版本

ng g m dialog;
ng g c dialog/dialog --standalone false -m dialog;
ng g c dialog/dialog-public --standalone false -m dialog;
ng g c dialog/dialog-private --standalone false -m dialog;
View Code

這裏有幾個知識點:

  1. dialog/dialog 是 path 的概念
    folder 結構是這樣的


    dialog 組件要放進 dialog folder

  2.  --standalone=false

    組件要使用 NgModule 做管理,就不可以是 Standalone Component。
    Standalone 的意思是獨立,也就是沒人管。

  3. --module=dialog
    這句的意思是,這個組件交給 dialog module 管理。 

NgModule Definition

這是一個完整的 NgModule Definition

import { NgModule } from '@angular/core';
import { DialogComponent } from './dialog/dialog.component';
import { DialogPublicComponent } from './dialog-public/dialog-public.component';
import { DialogPrivateComponent } from './dialog-private/dialog-private.component';
import { OtherModule } from '../other/other.module';              // 其它 NgModule
import { StandaloneComponent } from '../standalone/standalone.component'; // 其它 Standalone Component

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  imports: [OtherModule, StandaloneComponent],
  exports: [DialogComponent, DialogPublicComponent],
})
export class DialogModule {}

語法上看,NgModule 是一個 class with @NgModule Decorator,class 本身是空的,Definition 都寫在 @NgModule Decorator 的參數對象裏。

它有好幾個規則,我們一個一個看:

  1. declarations
    declarations 表示這個 NgModule 負責管理的組件集合。
    集合內的組件們可以相互使用,不需要在 @Component 做 imports。
    舉例:
    declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent]
    在 Dialog Template 使用 DialogPrivate 組件


    Dialog 組件不需要 import DialogPrivate 組件。

  2. exports

    declarations 的組件集合只是自己跟自己玩,要讓外面的人也能使用到 declarations 組件集合,我們需要 exports 它們。
    沒有 export 的組件,我們稱爲 private 組件 (在 NgModule 內跟自己人玩),有 export 的稱爲 public 組件。
    舉例:

    exports: [DialogComponent, DialogPublicComponent]

    外人 (App 組件) import 了 DialogModule。App Template 可以使用 Dialog 組件 和 DialogPublic 組件,但是無法使用 DialogPrivate 組件。
    因爲 DialogPrivate 組件沒有在 DialogModule 的 exports list 裏,它是 private 的,只有 declarations 的組件可以用它。

  3. imports
    imports 表示這個 NgModule 依賴的其它 NgModule 或者組件 (Standalone Component),
    所有 declarations 的組件都可以使用 imports 的 NgModule (內的 public 組件) 和 Standalone 組件。

  4. re-export
    exports list 不僅僅可以放 declarations 的組件,也可以放 imports 的 NgModule 和 組件。
    import 了又 export 就叫 re-export。
    舉例:
    imports: [OtherModule, StandaloneComponent],
    exports: [DialogComponent, DialogPublicComponent, OtherModule],

    假如 App 組件 import 了 DialogModule,App Template 可以使用 Dialog 組件,DialogPublic 組件 和 OtherModule export 的組件和 NgModule。

通過以上幾個規則,NgModule 就解決了 Standalone Component 的 2 大問題:

  1. A lot of component imports
    App Template 要使用 Dialog 組件集合,不需要一個一個組件 import,只要 import 一個 DialogModule 就可以了。
  2. Private 組件
    DialogPrivate 組件不是 Standalone Component,App 組件無法直接 import 它。
    DialogPrivate 也不在 DialogModule 的 exports list 裏,即使 App 組件 import DialogModule 依然無法使用 DialogPrivate。
    只有 DialogModule declarations 組件集合纔可以使用到 DialogPrivate。

 

NgModule 源碼逛一逛

上面我們提到的各種 NgModule 對組件管理的規則檢測,並不是發生在 runtime,而是在 compilation 之前 Angular 就已經通過 Angular Language Service 做靜態分析 (Static Program Analysis) 了。

規則 1:NgModule declarations 的組件集合可以相互使用。

假設 DialogModule 沒有 declare DialogPublic 和 DialogPrivate。

@NgModule({
  declarations: [
    DialogComponent,
    // DialogPublicComponent,
    // DialogPrivateComponent
  ]
})
export class DialogModule {}

當我們在 Dialog Template 嘗試使用 DialogPublic 或 DialogPrivate 時,IDE 就會報錯。

declare DialogPublic 和 DialogPrivate 組件

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
})
export class DialogModule {}

declare 後 error 就沒有了

規則 2:NgModule 有 export 的纔是 public 組件,沒有 export 就是 private 組件

假設 DialogModule 沒有 exports

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  exports: [
    // DialogComponent,
    // DialogPublicComponent
  ],
})
export class DialogModule {}

即便 App 組件 import 了 DialogModule 也無法使用到 Dialog 組件。

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [DialogModule], // imported DialogModule
})
export class AppComponent {
  constructor() {}
}

error on App Template

三個組件都報錯了。

export Dialog 和 DialogPublic 組件。但不要 export DialogPrivate 組件。

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent],
  exports: [DialogComponent, DialogPublicComponent],
})
export class DialogModule {}

效果

只剩下 DialogPrivate 報錯,因爲它是 private 組件。

規則 3:NgModule imports 的組件 / NgModule,declarations 都可以使用

假設有一個 Standalone 組件和一個 OtherModule

Standalone 組件

OtherModule

Dialog Template 想使用 Standalone 組件和 Other 組件

在 DialogModule import Standalone 組件和 OtherModule (Other 組件不是 Standalone Component,所以不可以直接被 import,只可以 import 它的管理負責人 -- OtherModule)

import 後,Dialog Template 就可以使用 Standalone 組件和 Other 組件了。

規則 4:NgModule re-export

假設 App 組件 import 了 DialogModule,App Template 想使用 Other 組件

DialogModule re-export OtherModule

re-export 後,App Template 就可以使用 Other 組件了。

NgModule After Compilation

我們來看看 compile 後的 NgModule 長啥樣。

這是 DialogModule,它 export 了很多組件。

@NgModule({
  declarations: [DialogComponent, DialogPublicComponent, DialogPrivateComponent,
  ],
  exports: [DialogComponent, DialogPublicComponent, OtherModule],
  imports: [StandaloneComponent, OtherModule],
})
export class DialogModule {}

App 組件 import 了 DialogModule

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
  imports: [DialogModule],
})
export class AppComponent {
  constructor() {}
}

但是 App Template 只用到了一個 Dialog 組件

<h1>Hello World</h1>
<app-dialog />

運行 compile

yarn run ngc -p tsconfig.json

app.component.js

App Definition 的 dependencies 有一個 DialogModule 和 DialogComponent。

dialog.module.js

可以看到,雖然 DialogModule export 了很多組件,但由於 App Template 只用了其中的 Dialog 組件,所以 App Definition 只依賴了一個 Dialog 組件 (這一點和 import Standalone Component 的規則是完全一樣的) 和 DialogModule。

而 DialogModule 裏並沒有任何 declarations 組件的信息。也就是說那些信息只用於 Angular Language Service 做靜態分析而已,compile 後就不需要了。

兩個結論:

  1. NgModule 管理組件只限於 Angular Language Service 靜態分析,compile 以後它的行爲和 Standalone Component 是完全一樣的。
  2. 既然和 Standalone Component 一樣,那爲什麼 compile 後還需要生成 DialogModule?而且 App Definition 的 dependencies 也包含了 DialogModule 丫?
    這是因爲 NgModule 除了可以拿來管理組件,它還可以拿來搞 Dependency Injection 依賴注入 (爲什麼我用 "搞" 而不是 "管理"?因爲我覺得它把東西越搞越亂就有😵)。
    好,下一 part 我們就繼續講解這個 NgModule の Dependency Injection,Let's check it out 🚀

 

NgModule の Dependency Injection

不熟悉 Dependency Injection (DI) 的朋友,可以先看這兩篇 Dependency Injection 依賴注入 和 Component 組件 の Dependency Injection & NodeInjector

我們學習過 3 種提供 Provider 的方式:

  1. 通過 @Injectable 的 providedIn


    或者 InjectionToken 的 providedIn

  2. 通過 ApplicationConfig.providers

  3. 通過組件 / 指令 Decorator 的 providers

使用 NgModule 提供 Provder 是我們將學習到的四種方式。

雖然它是第四種方式,但其實它誕生的比 providedIn 和 ApplicationConfig.providers 都要早,只是它落伍了,所以我們才一直不需要去學它。

NgModule providers 長這樣

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

目錄

上一篇 Angular 17+ 高級教程 – HttpClient

下一篇 TODO

想查看目錄,請移步 Angular 17+ 高級教程 – 目錄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章