填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

使用輕量級注入令牌優化客戶應用的大小

Optimizing client app size with lightweight injection tokens

本頁面會提供一個概念性的概述,它介紹了一種建議函式庫開發者使用的依賴注入技術。使用輕量級注入令牌設計你的函式庫,這有助於優化那些用到你函式庫的客戶應用的發佈套件體積。

This page provides a conceptual overview of a dependency injection technique that is recommended for library developers. Designing your library with lightweight injection tokens helps optimize the bundle size of client applications that use your library.

你可以使用可搖樹優化的提供者來管理元件和可注入服務之間的依賴結構,以優化發佈套件體積。這通常會確保如果提供的元件或服務從未被應用實際使用過,那麼編譯器就可以從發佈套件中刪除它的程式碼。

You can manage the dependency structure among your components and injectable services to optimize bundle size by using tree-shakable providers. This normally ensures that if a provided component or service is never actually used by the app, the compiler can eliminate its code from the bundle.

但是,由於 Angular 儲存注入令牌的方式,可能會導致未用到的元件或服務最終進入發佈套件中。本頁描述了依賴注入的一種設計模式,它透過使用輕量級注入令牌來支援正確的搖樹優化。

However, due to the way Angular stores injection tokens, it is possible that such an unused component or service can end up in the bundle anyway. This page describes a dependency-injection design pattern that supports proper tree-shaking by using lightweight injection tokens.

這種輕量級注入令牌設計模式對於函式庫開發者來說尤其重要。它可以確保當應用只用到了你函式庫中的某些功能時,可以從客戶應用的發佈套件中刪除未使用過的程式碼。

The lightweight injection token design pattern is especially important for library developers. It ensures that when an application uses only some of your library's capabilities, the unused code can be eliminated from the client's application bundle.

當某應用用到了你的函式庫時,你的函式庫中可能會提供一些客戶應用未用到的服務。在這種情況下,應用開發人員會期望該服務是可搖樹優化的,不讓這部分程式碼增加應用的編譯後大小。由於應用開發人員既無法瞭解也無法解決函式庫的搖樹優化問題,因此這是函式庫開發人員的責任。為了防止未使用的元件被保留下來,你的函式庫應該使用輕量級注入令牌這種設計模式。

When an application uses your library, there might be some services that your library supplies which the client application doesn't use. In this case, the application developer should expect that service to be tree-shaken, and not contribute to the size of the compiled application. Because the application developer cannot know about or remedy a tree-shaking problem in the library, it is the responsibility of the library developer to do so. To prevent the retention of unused components, your library should use the lightweight injection token design pattern.

什麼時候令牌會被保留

When tokens are retained

為了更好地解釋令牌被保留的條件,我們考慮一個提供卡片元件的函式庫,它包含一個卡片體,還可以包含一個可選的卡片頭。

To better explain the condition under which token retention occurs, consider a library that provides a library-card component, which contains a body and can contain an optional header.

      
      <lib-card>
  <lib-header>...</lib-header>
</lib-card>
    

在一個可能的實現中, <lib-card> 元件使用 @ContentChild() 或者 @ContentChildren() 來獲取 <lib-header><lib-body> ,如下所示。

In a likely implementation, the <lib-card> component uses @ContentChild() or @ContentChildren() to obtain <lib-header> and <lib-body>, as in the following.

      
      @Component({
  selector: 'lib-header',
  ...,
})
class LibHeaderComponent {}

@Component({
  selector: 'lib-card',
  ...,
})
class LibCardComponent {
  @ContentChild(LibHeaderComponent)
  header: LibHeaderComponent|null = null;
}
    

因為 <lib-header> 是可選的,所以元素可以用最小化的形式 <lib-card></lib-card> 出現在範本中。在這個例子中,<lib-header> 沒有用過,你可能期望它會被搖樹優化掉,但事實並非如此。這是因為 LibCardComponent 實際上包含兩個對 LibHeaderComponent 參考。

Because <lib-header> is optional, the element can appear in the template in its minimal form, <lib-card></lib-card>. In this case, <lib-header> is not used and you would expect it to be tree-shaken, but that is not what happens. This is because LibCardComponent actually contains two references to the LibHeaderComponent.

@ContentChild(LibHeaderComponent) header: LibHeaderComponent;

  • 其中一個參考位於型別位置上 - 即,它把 LibHeaderComponent 用作了型別: header: LibHeaderComponent;

    One of these reference is in the type position-- that is, it specifies LibHeaderComponent as a type: header: LibHeaderComponent;.

  • 另一個參考位於值的位置 - 即,LibHeaderComponent 是 @ContentChild() 引數裝飾器的值: @ContentChild(LibHeaderComponent)

    The other reference is in the value position-- that is, LibHeaderComponent is the value of the @ContentChild() parameter decorator: @ContentChild(LibHeaderComponent).

編譯器對這些位置的令牌參考的處理方式也不同。

The compiler handles token references in these positions differently.

  • 編譯器在從 TypeScript 轉換完後會刪除這些型別位置上的參考,所以它們對於搖樹優化沒什麼影響。

    The compiler erases type position references after conversion from TypeScript, so they have no impact on tree-shaking.

  • 編譯器必須在執行時保留值位置上的參考,這就會阻止該元件被搖樹優化掉。

    The compiler must retain value position references at runtime, which prevents the component from being tree-shaken.

在這個例子中,編譯器保留了 LibHeaderComponent 令牌,它出現在了值位置上,這就會防止所參考的元件被搖樹優化掉,即使應用開發者實際上沒有在任何地方用過 <lib-header>。如果 LibHeaderComponent 很大(程式碼、範本和樣式),把它包含進來就會不必要地大大增加客戶應用的大小。

In the example, the compiler retains the LibHeaderComponent token that occurs in the value position, which prevents the referenced component from being tree-shaken, even if the application developer does not actually use <lib-header> anywhere. If LibHeaderComponent is large (code, template, and styles), including it unnecessarily can significantly increase the size of the client application.

什麼時候使用輕量級注入令牌模式

When to use the lightweight injection token pattern

當一個元件被用作注入令牌時,就會出現搖樹優化的問題。有兩種情況可能會發生。

The tree-shaking problem arises when a component is used as an injection token. There are two cases when that can happen.

  • 令牌用在內容查詢中值的位置上。

    The token is used in the value position of a content query.

  • 該令牌用作建構函式注入的型別說明符。

    The token is used as a type specifier for constructor injection.

在下面的例子中,兩處對 OtherComponent 令牌的使用導致 OtherComponent 被保留下來(也就是說,防止它在未用到時被搖樹優化掉)。

In the following example, both uses of the OtherComponent token cause retention of OtherComponent (that is, prevent it from being tree-shaken when it is not used).

      
      class MyComponent {
  constructor(@Optional() other: OtherComponent) {}

  @ContentChild(OtherComponent)
  other: OtherComponent|null;
}
    

雖然轉換為 JavaScript 時只會刪除那些只用作型別說明符的令牌,但在執行時依賴注入需要所有這些令牌。這些工作把 constructor(@Optional() other: OtherComponent) 改成了 constructor(@Optional() @Inject(OtherComponent) other) 。該令牌現在處於值的位置,並使該搖樹優化器保留該參考。

Although tokens used only as type specifiers are removed when converted to JavaScript, all tokens used for dependency injection are needed at runtime. These effectively change constructor(@Optional() other: OtherComponent) to constructor(@Optional() @Inject(OtherComponent) other). The token is now in a value position, and causes the tree shaker to retain the reference.

對於所有服務,函式庫都應該使用可搖樹優化的提供者,在根級而不是元件建構函式中提供依賴。

For all services, a library should use tree-shakable providers, providing dependencies at the root level rather than in component constructors.

使用輕量級注入令牌

Using lightweight injection tokens

輕量級注入令牌設計模式包括:使用一個小的抽象類別作為注入令牌,並在稍後為它提供實際實現。該抽象類別固然會被留下(不會被搖樹優化掉),但它很小,對應用程式的大小沒有任何重大影響。

The lightweight injection token design pattern consists of using a small abstract class as an injection token, and providing the actual implementation at a later stage. The abstract class is retained (not tree-shaken), but it is small and has no material impact on the application size.

下例舉例說明了這個 LibHeaderComponent 的工作原理。

The following example shows how this works for the LibHeaderComponent.

      
      abstract class LibHeaderToken {}

@Component({
  selector: 'lib-header',
  providers: [
    {provide: LibHeaderToken, useExisting: LibHeaderComponent}
  ]
  ...,
})
class LibHeaderComponent extends LibHeaderToken {}

@Component({
  selector: 'lib-card',
  ...,
})
class LibCardComponent {
  @ContentChild(LibHeaderToken) header: LibHeaderToken|null = null;
}
    

在這個例子中, LibCardComponent 的實現裡,LibHeaderComponent 既不會出現在型別的位置也不會出現在值的位置。這樣就可以讓 LibHeaderComponent 完全被搖樹優化掉。LibHeaderToken 被留下了,但它只是一個類別宣告,沒有具體的實現。它很小,並且在編譯後保留時對應用程式的大小沒有實質影響。

In this example, the LibCardComponent implementation no longer refers to LibHeaderComponent in either the type position or the value position. This allows full tree shaking of LibHeaderComponent to take place. The LibHeaderToken is retained, but it is only a class declaration, with no concrete implementation. It is small and does not materially impact the application size when retained after compilation.

不過,LibHeaderComponent 本身實現了抽象類別 LibHeaderToken。你可以放心使用這個令牌作為元件定義中的提供者,讓 Angular 能夠正確地注入具體型別。

Instead, LibHeaderComponent itself implements the abstract LibHeaderToken class. You can safely use that token as the provider in the component definition, allowing Angular to correctly inject the concrete type.

總結一下,輕量級注入令牌模式由以下幾部分組成。

To summarize, the lightweight injection token pattern consists of the following.

  1. 一個輕量級的注入令牌,它表現為一個抽象類別。

    A lightweight injection token that is represented as an abstract class.

  2. 一個實現該抽象類別的元件定義。

    A component definition that implements the abstract class.

  3. 注入這種輕量級模式時使用 @ContentChild() 或者 @ContentChildren()

    Injection of the lightweight pattern, using @ContentChild() or @ContentChildren().

  4. 實現輕量級注入令牌的提供者,它將輕量級注入令牌和它的實現關聯起來。

    A provider in the implementation of the lightweight injection token which associates the lightweight injection token with the implementation.

使用輕量級注入令牌進行 API 定義

Use the lightweight injection token for API definition

那些注入了輕量級注入令牌的元件可能要呼叫注入的類別中的方法。因為令牌現在是一個抽象類別,並且可注入元件實現了那個抽象類別,所以你還必須在作為輕量級注入令牌的抽象類別中宣告一個抽象方法。該方法的實現程式碼(及其所有相關程式碼)都會留在可注入元件中,但這個元件本身仍可被搖樹優化。這樣就能讓父元件以型別安全的方式與子元件(如果存在)進行通訊。

A component that injects a lightweight injection token might need to invoke a method in the injected class. Because the token is now an abstract class, and the injectable component implements that class, you must also declare an abstract method in the abstract lightweight injection token class. The implementation of the method (with all of its code overhead) resides in the injectable component that can be tree-shaken. This allows the parent to communicate with the child (if it is present) in a type-safe manner.

例如,LibCardComponent 現在要查詢 LibHeaderToken 而不是 LibHeaderComponent 。這個例子展示了該模式如何讓 LibCardComponentLibHeaderComponent 通訊,卻不用實際參考 LibHeaderComponent

For example, the LibCardComponent now queries LibHeaderToken rather than LibHeaderComponent. The following example shows how the pattern allows LibCardComponent to communicate with the LibHeaderComponent without actually referring to LibHeaderComponent.

      
      abstract class LibHeaderToken {
  abstract doSomething(): void;
}

@Component({
  selector: 'lib-header',
  providers: [
    {provide: LibHeaderToken, useExisting: LibHeaderComponent}
  ]
  ...,
})
class LibHeaderComponent extends LibHeaderToken {
  doSomething(): void {
    // Concrete implementation of `doSomething`
  }
}

@Component({
  selector: 'lib-card',
  ...,
})
class LibCardComponent implement AfterContentInit {
  @ContentChild(LibHeaderToken)
  header: LibHeaderToken|null = null;

  ngAfterContentInit(): void {
    this.header && this.header.doSomething();
  }
}
    

在這個例子中,父元件會查詢令牌以獲取子元件,並持有結果元件的參考(如果存在)。在呼叫子元件中的方法之前,父元件會檢查子元件是否存在。如果子元件已經被搖樹優化掉,那執行期間就沒有對它的參考,當然也沒有呼叫它的方法。

In this example the parent queries the token to obtain the child component, and stores the resulting component reference if it is present. Before calling a method in the child, the parent component checks to see if the child component is present. If the child component has been tree-shaken, there is no runtime reference to it, and no call to its method.

為你的輕量級注入令牌命名

Naming your lightweight injection token

輕量級注入令牌只對元件有用。Angular 風格指南中建議你使用“Component”字尾命名元件。例如“LibHeaderComponent”就遵循這個約定。

Lightweight injection tokens are only useful with components. The Angular style guide suggests that you name components using the "Component" suffix. The example "LibHeaderComponent" follows this convention.

為了維護元件及其令牌之間的對應關係,同時又要區分它們,推薦的寫法是使用元件基本名加上字尾“Token”來命名你的輕量級注入令牌:“LibHeaderToken”。

To maintain the relationship between the component and its token while still distinguishing between them, the recommended style is to use the component base name with the suffix "Token" to name your lightweight injection tokens: "LibHeaderToken".