本頁從概念上總覽一項建議函式庫開發者採用的依賴注入技巧。 以輕量注入 token 來設計你的函式庫,有助於最佳化使用你函式庫的用戶端應用程式的 bundle 大小。
你可以透過可搖樹的提供者,管理元件與可注入服務之間的依賴結構,藉此最佳化 bundle 大小。 這通常能確保:如果提供的元件或服務從未被應用程式實際使用,編譯器就能把它的程式碼從 bundle 中移除。
礙於 Angular 儲存注入 token 的方式,這類未使用的元件或服務還是有可能最終出現在 bundle 中。 本頁說明一種依賴注入設計模式,透過輕量注入 token 來支援正確的搖樹優化。
輕量注入 token 設計模式對函式庫開發者尤其重要。 它能確保當應用程式只使用你函式庫的部分功能時,未使用的程式碼可以從用戶端應用程式的 bundle 中消除。
當應用程式使用你的函式庫時,你的函式庫提供的某些服務,用戶端應用程式可能並不會用到。 在這種情況下,應用程式開發者理應期待該服務被搖樹優化移除,而不是增加編譯後應用程式的大小。 由於應用程式開發者無從得知函式庫中的搖樹優化問題、也無法加以補救,防止未使用元件被保留下來是函式庫開發者的責任。 為此,你的函式庫應該採用輕量注入 token 設計模式。
token 何時會被保留
為了更清楚地說明 token 被保留的條件,設想一個提供 library-card 元件的函式庫。 這個元件包含一個主體,還可以包含一個可選的標頭:
<lib-card>
<lib-header>…</lib-header>
</lib-card>
一種可能的實作是,<lib-card> 元件使用 contentChild 或 contentChildren 來取得 <lib-header> 和 <lib-body>,如下所示:
import {Component, contentChild} from '@angular/core';
@Component({
selector: 'lib-header',
…,
})
class LibHeader {}
@Component({
selector: 'lib-card',
…,
})
class LibCard {
readonly header = contentChild(LibHeader);
}
由於 <lib-header> 是可選的,該元素在範本中可以以最精簡的形式出現:<lib-card />。
在這種情況下,<lib-header> 沒有被使用,你會期待它被搖樹優化移除,但實際並非如此。
這是因為 LibCard 其實包含兩處對 LibHeader 的參考:
readonly header = contentChild(LibHeader);
- 其中一處參考位於型別位置,也就是它把
LibHeader指定為型別:readonly header: Signal<LibHeader|undefined>。 - 另一處參考位於值位置,也就是
LibHeader是傳入contentChild函式的值:contentChild(LibHeader)。
編譯器對這兩個位置的 token 參考處理方式不同:
- 編譯器會在從 TypeScript 轉換後清除型別位置的參考,因此它們對搖樹優化沒有影響。
- 編譯器必須在執行階段保留值位置的參考,這會阻止元件被搖樹優化移除。
在這個範例中,編譯器保留了出現在值位置的 LibHeader token。
這會阻止被參考的元件被搖樹優化移除,即使應用程式實際上沒有在任何地方使用 <lib-header>。
如果 LibHeader 的程式碼、範本和樣式合計起來太大,不必要地包含它會顯著增加用戶端應用程式的大小。
何時該使用輕量注入 token 模式
當元件被當作注入 token 使用時,就會出現搖樹優化問題。 有兩種情況會發生這種事:
- token 用在內容查詢的值位置。
- token 搭配
inject函式使用。
在以下範例中,CustomOther token 的兩種用法都會造成 CustomOther 被保留,在它未被使用時阻止它被搖樹優化移除:
class App {
private readonly other = inject(CustomOther, {optional: true});
readonly header = contentChild(CustomOther);
}
雖然只用作型別指定項的 token 在轉換為 JavaScript 時會被移除,但所有用於依賴注入的 token 在執行階段都是必需的。
使用 inject(CustomOther) 時,CustomOther 是以值引數的形式傳入。
token 此時位於值位置,這會導致搖樹優化器保留該參考。
HELPFUL: 函式庫的所有服務都應該使用可搖樹的提供者,在根層級提供依賴,而不是在元件或模組中提供。
使用輕量注入 token
輕量注入 token 設計模式的做法是:使用一個小的抽象類別作為注入 token,並在稍後的階段提供實際實作。 抽象類別會被保留、不會被搖樹優化移除,但它很小,對應用程式大小沒有實質影響。
以下範例示範這如何應用於 LibHeader:
abstract class LibHeaderToken {}
@Component({
selector: 'lib-header',
providers: [{provide: LibHeaderToken, useExisting: LibHeader}],
…,
})
class LibHeader extends LibHeaderToken {}
@Component({
selector: 'lib-card',
…,
})
class LibCard {
readonly header = contentChild(LibHeaderToken);
}
在這個範例中,LibCard 的實作不再於型別位置或值位置參考 LibHeader。
這讓 LibHeader 可以被完整地搖樹優化移除。
LibHeaderToken 會被保留,但它只是一個類別宣告,沒有具體實作。
它很小,在編譯後被保留下來時,不會對應用程式大小造成實質影響。
反過來,LibHeader 本身實作了抽象的 LibHeaderToken 類別。
你可以放心地把該 token 用作成元件定義中的提供者,讓 Angular 正確地注入具體型別。
總結來說,輕量注入 token 模式包含以下幾項:
- 一個以抽象類別表示的輕量注入 token。
- 一個實作該抽象類別的元件定義。
- 使用
contentChild或contentChildren注入輕量模式。 - 輕量注入 token 實作中的一個提供者,它把輕量注入 token 與實作關聯起來。
使用輕量注入 token 定義 API
注入輕量注入 token 的元件,可能需要叫用被注入類別中的某個方法。 token 現在是一個抽象類別。由於可注入元件實作了該類別,你還必須在抽象的輕量注入 token 類別中宣告一個抽象方法。 方法的實作連同其所有程式碼開銷,都位於可以被搖樹優化移除的可注入元件中。 這讓父元件能在子元件存在時,以型別安全的方式與它通訊。
例如,LibCard 現在查詢的是 LibHeaderToken,而不是 LibHeader。
以下範例示範這個模式如何讓 LibCard 在不實際參考 LibHeader 的情況下與 LibHeader 通訊:
abstract class LibHeaderToken {
abstract doSomething(): void;
}
@Component({
selector: 'lib-header',
providers: [{provide: LibHeaderToken, useExisting: LibHeader}],
})
class LibHeader extends LibHeaderToken {
doSomething(): void {
// Concrete implementation of `doSomething`
}
}
@Component({
selector: 'lib-card',
})
class LibCard implements AfterContentInit {
readonly header = contentChild(LibHeaderToken);
ngAfterContentInit(): void {
if (this.header() !== undefined) {
this.header()!.doSomething();
}
}
}
在這個範例中,父元件查詢 token 以取得子元件,並在子元件存在時儲存取得的元件參考。 在叫用子元件的方法之前,父元件會先檢查子元件是否存在。 如果子元件已被搖樹優化移除,執行階段就沒有對它的參考,也就不會叫用它的方法。
為你的輕量注入 token 命名
輕量注入 token 只對元件有用。
Angular 風格指南建議為元件命名時不要加上 Component 後綴。
範例中的 LibHeader 就遵循了這個慣例。
你應該維持元件與其 token 之間的關係,同時又能區分兩者。
建議的風格是使用元件的基礎名稱加上 Token 後綴,來為你的輕量注入 token 命名:LibHeaderToken。