人子來,不是要受人的服事,乃是要服事人。(馬太福音 廿:28)
Angular 17 Deferrable Views
Angular 17 推出 Deferrable views 的功能,使在樣版裡可以指定 條件依賴 延遲的區塊,包括了載入(components, directivespipes, 和有關連的 CSS),把所有需要 延遲的區塊放置 @defer { ... } 中。
 
可延遲視圖有支援許多條件的觸發器(on idleon viewporton interactionon hoveron immediateon timer) 和 子區塊(@placeholder@loading@error),也可以使用 whenprefetch when 自定條件。
 
看完本文章之後,就可以明白要如何使用 @defer 的語法,以及 內建(predefined) 和 自定(custom) 的 觸發器(triggers),和比較 @defer 和 lazy loading的不同。
[ TypeScript ]
@defer { <large-component /> }
那為什麼我們需要用新的 @defer 來進行可延遲檢視圖(Deferrable Views),因為 Angular 本來就有了路由式的延遲載入(router-based lazy loading), 可是我們還是要更快把網頁載入速度減少的方法,當使用者來參觀你的網頁,3秒沒有展開就會關掉網頁。
 
所以當你的頁面上的區塊越多,載入完成就會越慢,所以我們要想儘辦法加速載入的速度,終於在 Angular 17 支援頁面區塊 - 可延遲檢視圖(Deferrable Views),不過還是要小心延遲載入造成網頁版本變形破版。
 
@defer 能夠做到什麼呢?
 
  • 當使用者 "瀏覽" 到某一個位置時,載入較大的區塊。
  • 當使用者 "點擊" 某一個位置時,載入較大的區塊。
  • 當使用者在使用時,在背景中 "預先" 載入較大的區塊。
在下面的例子,我們建立了一個 sub-block 的元件(component),使用 @defer 將 sub-block 延遲載入。
[ TypeScript ]
import { Component } from '@angular/core'; import {} from '@angular/common'; import { RouterOutlet } from '@angular/router'; import { SubBlockComponent } from './sub-block.component'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, SubBlockComponent], template: ` <div> <div><router-outlet></router-outlet></div> <div> @defer { <app-sub-block></app-sub-block> } </div> </div> `, styles: [], }) export class AppComponent { title = 'app'; }
讓我們執行 ng serve 來看看下圖的 紅色框框 的 sub-block 的元件,現在就是在 Lazy chuck files 的區塊了。
ArticleImage
讓我們打開網頁來試試結果,下圖 sub-block元件就在最後才載入。
ArticleImage
@defer 提供了我們許多的功能( @placeloader@loading@error ),讓我們在許多功能需求都可以達成。
 

@defer with @placeloader

 
@placeloader 是與 @defer 一起使用的,提供我們在延遲載入時預設顯示的畫面,當進行載入時就會消失的區塊,我們可以拿來做簡單的區塊或是文字,來呈現在畫面上。
[ TypeScript ]
@defer { <app-sub-block></app-sub-block> } @placeholder { <span>This is a sub-block component.</span> }
在上面的例子,@placeholder 裡的 "This is a sub-block component.",會在一開始就出現,不過只會出現一瞬間出現,就會消失了,這樣會造成使用者覺是不是發生什麼錯誤了。
 
@placeholder parameters 
 
所以在 @placeholder 有提供了一個參數為 "minimum",時間週期以秒(s)和毫秒(ms)為單位。
 
下面的例子 @placeholder 的 minimum 接受2種方式
  • 數值 + s   ==>    2s       (秒)
  • 數值         ==>    2000  (毫秒)
[ TypeScript ]
# in seconds @defer { <app-sub-block></app-sub-block> } @placeholder ( minimum 2s ) { <span>This is a sub-block component.</span> } # in milliseconds @defer { <app-sub-block></app-sub-block> } @placeholder ( minimum 2000 ) { <span>This is a sub-block component.</span> }

@defer with @loading

 
@defer 另一個搭配使用 @loading,觸發時機為,在載入 "延遲載入" 元件,就是在下載單獨的 js 時會顯示在畫面上。
[ TypeScript ]
@defer { <app-sub-block></app-sub-block> } @loading { <span>Loading...</span> }
上面的例子裡,當在下載 <app-sub-block></app-sub-block> 時,才會顯示 <span>Loading...</span>。
 
不過在測式環境不增加任何的參數,難以顯示畫面,連一閃都看不到,因為下載的速度太快了。
 
如果還是要測試出來的話,那就要調整 Google Dveloper Tools-> Network -> 模擬 Slow 3G 的速度才會顯示出 <span>Loading...</span> 在畫面上。
ArticleImage
@loading parameters
 
@loading 提供了2個可選參數
  • minimum 設定顯示區塊最短的時間。
  • after 當觸發下載時,等待多少時間才顯示區塊
[ TypeScript ]
# minimum 5s @defer { <app-sub-block></app-sub-block> } @loading (minimum 5s) { <span>Loading...</span> } # after 2s; minimum 5s @defer { <app-sub-block></app-sub-block> } @loading (after 2s; minimum 5s) { <span>Loading...</span> }
上面的是單獨設定 minimum 和 after & minimum的例子,設定的方式都是一樣的 秒 和 毫秒。
 
不過上面 設定 after 為 2秒,下載這個元件的真實時間為 1秒,這樣的話 minimum 不管設定什麼都會失效。
 
@placeholder & @loading 有什麼不同,以目前看來功能一差不多,就是不同階段顯示不同的畫面區塊,不過個人是以下簡單的流程就會比較清楚的不同。
 
@placeholder(非必要)(條件式) => @defer(必要)(條件式) => @loading(非必要)(條件式)
 

@defer with @error

 
@error 這個區塊就是當發生下載錯誤時 或 無論是什麼原因造成的錯誤,就會觸發 @error 區塊。
[ TypeScript ]
@defer { <app-sub-block></app-sub-block> } @error { <span>Error...</span> }

How do @defer triggers work?

接下來終於進入如何觸發 @defer 的選項的功能,可分為下面二大部份:
  • (可選的) 何時預先(prefetch)載入。
  • (可選的) 何時顯示區塊的觸發器。
這二大部份是非常不同的事件,可以分別控制並靈活使用,系統有提供預設的觸發器(使用關鍵字 on),也可以自定觸發器(使用關鍵字 when)。
 
系統預設的觸發器有下列(使用關鍵字 on):
  • idle (閒置)
  • viewport (區塊顯示)
  • interaction (互動)
  • hover (移進移出)
  • immediate (主即)
  • timer (計時器)
我們會一一來學習這些系統預定觸發器
 

預設觸發器 idle ( The idle built-in trigger )

[ TypeScript ]
@defer { <app-sub-block></app-sub-block> } // This is equivalent to: @defer (on idle; prefetch on idle) { <app-sub-block></app-sub-block> }
上面的例子,Angular 使用瀏覽器 api(requestIdleCallback)檢測瀏覽器,完成所有的下載的連線時,通常就會出現 "閒置" 的狀態,所以 @defer 通常都立即觸發。
 

預設觸發器 viewport@placeholder 相關( The viewport built-in trigger with @placeholder )

 
viewport 觸發器 可以和 @placeholder 搭配使用,當 @placeholder 區塊顯示在畫面上時,就會觸發 viewport 執行下載區塊的程式。
[ TypeScript ]
@defer (on viewport) { <app-sub-block></app-sub-block> } @placeholder { <span>waiting...</span> }
viewport 觸發器可以單獨使用,不過要 指定 一個區塊, 當 指定 的區塊 顯示 在畫面上時,viewport 就會觸發下載區塊的程式。
[ TypeScript ]
<div #title>Title</div> @defer (on viewport(title)) { <app-sub-block></app-sub-block> }

預設觸發器 interaction@placeholder 相關( The interaction built-in trigger with @placeholder )

 
interaction 可以搭配 @placeholder 使用,當 @placeholder 的區塊有 互動 (點擊輸入、…),interaction 就會觸發下載 @defer 區塊程式。
[ TypeScript ]
@defer (on interaction) { <app-sub-block></app-sub-block> } @placeholder { <div>click</div> }
interaction 可以單獨使用,同樣的要綁定一個元件,當元件跟使用者 互動 (點擊輸入…)時,interaction 就會觸發 @defer 下載區塊程式。
[ TypeScript ]
<div #click>click</div> @defer (on interaction(click)) { <app-sub-block></app-sub-block> }

預設觸發器 hover@placeholder 相關( The hover built-in trigger with @placeholder )

 
hover 可以搭配 @placeholder 使用,當 滑鼠移進 ( 事件 mouseenter、focusin ) @placeholder 的區塊時,hover 就會觸發下載 @defer 區塊程式。
[ TypeScript ]
@defer (on hover) { <app-sub-block></app-sub-block> } @placeholder { <div>hover</div> }
hover 可以單獨使用,同樣的要綁定一個元件,當 滑鼠移進 ( 事件 mouseenter、focusin ) 元件時,hover 就會觸發 @defer 下載區塊程式。
[ TypeScript ]
<div #action>hover</div> @defer (on hover(action)) { <app-sub-block></app-sub-block> }

預設觸發器 immediate ( The immediate built-in trigger )

 
immediate立即 觸發 @defer 下載區塊程式,不會被等任何事件,也不會等待 閒置 才載入(idle)。
[ TypeScript ]
@defer (on immediate) { <app-sub-block></app-sub-block> }

預設觸發器 timer ( The timer built-in trigger )

 
timer 就是字面上定義的 計時器,當 計時器 設定的時間結束,timer 就會觸發 @defer 下載區塊程式。
 
計時器(timer)可以接受 毫秒(ms) 或 (s)。
 
下面2個例子是一樣的,只是使用 (s) 或 毫秒(ms) 的參數。
[ TypeScript ]
// 秒(s) @defer (on timer(5s)) { <app-sub-block></app-sub-block> } // 毫秒(ms) @defer (on timer(5000)) { <app-sub-block></app-sub-block> }

預取 @defer 區塊( Prefetching @defer blocks )

 
上面都是控制何時顯示區塊,接下來我們要學習 預取 ,預取就是預先下載完成,使用 @defer 時,我們有2個層次的控制:
  • 控制何時預取將下載完成。
  • 控制何時將區塊顯示。
[ TypeScript ]
@defer (on timer(5s)) { <app-sub-block></app-sub-block> } // Is functionally equivalent to @defer (on timer(5s); prefetch on idle) { <app-sub-block></app-sub-block> }
在上面的2個例子是相同的結果,2個例子都是 5秒後 顯示區塊 和 當有 閒置 時就預先下載程式。
 
下面的例子我們要做不同的 預先下載 的例子:
[ TypeScript ]
@defer (on interaction; prefetch on viewport) { <app-sub-block></app-sub-block> } @placeholder { <input /> }
@placeholder 區塊裡的 input 產生 互動 時就顯示 @defer 區塊程式(藍色區塊),當 @placeholder 區塊裡的 input 顯示時,就預先下載 @defer 區塊的程式(紅色區塊)。
 
上面的例子 預設的觸發器 都可以使用在 預取顯示 @defer 區塊,所以說我們可以 自已組合 所需的運用。
 

自定 @defer 觸發器(Custom @defer triggers)

 
Angular 的 @defer 提供可以 自定 觸發器,使用 when 關鍵字來定義相關條件,顯示預先下載區塊 都可分別設定,以下的例子可以看到如何設定:
[ TypeScript ]
@Component({ selector: 'app-root', standalone: true, imports: [SubBlockComponent], template: ` <div> <button (click)="onLoad()">Trigger Prefetch</button> <button (click)="onDisplay()">Trigger Display</button> @defer (when show; prefetch when load) { <app-sub-block></app-sub-block> } </div> `, styles: [], }) export class AppComponent { load: boolean = false; show: boolean = false; onLoad() { this.load = true; } onDisplay() { this.show = true; } }
上面的例子實作了決定 何時(when)預先(prefetch)下載程式、何時(when)顯示區塊,在 藍色區塊(when show) 條件式裡,當 Trigger Prefectch 按鈕被按下去時,就預先(prefetch)下載 <app-sub-block> 程式,在 紅色區塊(prefetch when load) 條件式裡,當 Trigger Display 按鈕被按下去時,就顯示 <app-sub-block> 區塊。