過去一年,Angular 推出了許多新功能,例如水合和可延遲的檢視畫面,協助開發人員改善Core Web Vitals,確保使用者享有絕佳體驗。我們也正在研究以這項功能為基礎的其他伺服器端算繪相關功能,例如串流和部分飲水。
遺憾的是,有一種模式可能會導致應用程式或程式庫無法充分利用所有這些全新和即將推出的功能:手動操控基礎 DOM 結構。Angular 規定,當元件被伺服器序列化時, DOM 的結構必須保持一致,直到瀏覽器為元件供水為止。如果在補充水之前使用 ElementRef
、Renderer2
或 DOM API 手動新增、移動或移除 DOM 中的節點,可能會導致這些功能出現不一致的問題。
不過,並非所有手動 DOM 操控和存取作業都會導致問題,有時甚至是必要的。安全使用 DOM 的關鍵,在於盡可能減少使用 DOM 的需求,並盡可能延後使用 DOM。以下指南將說明如何達成此目標,並建構符合未來趨勢且符合未來趨勢的 Angular 元件,充分運用 Angular 所有新功能和即將推出的功能。
避免手動操控 DOM
避免手動進行 DOM 操弄,的最佳方法是避免出乎意料,盡可能避免整體情況發生。Angular 內建 API 和模式,可操控 DOM 大部分的層面:我們建議您使用這些 API 和模式,而不要直接存取 DOM。
修改元件本身的 DOM 元素
編寫元件或指令時,您可能需要修改主機元素 (即與元件或指令的選取器相符的 DOM 元素),例如新增類別、樣式或屬性,而非指定或引入包裝元素。您可能會想直接使用 ElementRef
來變更底層 DOM 元素。請改用主機繫結,以宣告式方式將值繫結至運算式:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
就像HTML 中的資料繫結一樣,您也可以繫結至屬性和樣式,並將 'true'
變更為 Angular 會用來視需要自動新增或移除值的其他運算式。
在某些情況下,系統需要動態計算鍵。您也可以繫結至信號或函式,用來傳回一組值或對應值:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
在較複雜的應用程式中,您可能會想手動操作 DOM,以避免發生 ExpressionChangedAfterItHasBeenCheckedError
。您可以改為將值繫結至信號,如上一個範例所示。您可以視需要執行這項操作,而且不需要在整個程式碼庫中採用信號。
在範本以外修改 DOM 元素
您可能會想嘗試使用 DOM 存取通常無法存取的元素,例如屬於其他父項或子項元件的元素。不過,這種做法很容易出錯、違反封裝,而且日後難以變更或升級這些元件。
因此,您的元件應將所有其他元件視為黑箱。請花點時間思考其他元件 (即使是在同一個應用程式或程式庫中) 可能需要何時何地與您的元件互動或自訂元件的行為或外觀,然後提供安全且有文件說明的做法。如果簡單的 @Input
和 @Output
屬性不足以應付需求,請使用 階層依附元件插入等功能,讓 API 可供子集使用。
過去,實作模式對話方塊或工具提示等功能時,通常會在 <body>
或其他主機元素的結尾新增元素,然後將內容移至該處或投射至該處。不過,您現在可能會在範本中算繪簡單的 <dialog>
元素:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
延後手動 DOM 操控
按照上述指南盡量減少直接操作 DOM 和存取權限後,您可能仍會遇到無法避免的情況。在這種情況下,請務必盡可能延後。afterRender
和 afterNextRender
回呼是實現此目標的絕佳方法,因為 Angular 會先檢查任何變更,並將其提交至 DOM,然後才在瀏覽器上執行這些回呼。
執行僅限瀏覽器的 JavaScript
在某些情況下,您可能會有只能在瀏覽器中運作的程式庫或 API (例如圖表程式庫、某些 IntersectionObserver
用法等)。您可以使用 afterNextRender
,而非條件式檢查是否在瀏覽器上執行,或在伺服器上模擬行為:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
執行自訂版面配置
有時您可能需要讀取或寫入 DOM,才能執行目標瀏覽器尚不支援的版面配置,例如定位工具提示。afterRender
是這項作業的絕佳選擇,因為您可以確定 DOM 處於一致狀態。afterRender
和 afterNextRender
接受 EarlyRead
、Read
或 Write
的 phase
值。在寫入 DOM 版面配置後讀取,會迫使瀏覽器同步重新計算版面配置,這可能會嚴重影響效能 (請參閱「版面配置耗盡資源」)。因此,請務必仔細將邏輯分割至正確的階段。
舉例來說,如果工具提示元件想要顯示與網頁上其他元素相關的工具提示,就可能會使用兩個階段。EarlyRead
階段會先用來取得元素的大小和位置:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
接著,Write
階段會使用先前讀取的值,實際調整工具提示的位置:
afterRender(() => {
tooltipEl.style.setProperty('left', `${targetRect.left + targetRect.width / 2 - tooltipRect.width / 2}px`);
tooltipEl.style.setProperty('top', `${targetRect.bottom - 4}px`);
}, { phase: AfterRenderPhase.Write },
);
將邏輯分割成正確的階段,Angular 就能有效地在應用程式中的每個其他元件中批次處理 DOM,確保對效能造成的影響降到最低。
結論
Angular 伺服器端算繪功能即將推出許多令人期待的新功能,目的是讓您更輕鬆地為使用者提供優質體驗。希望上述提示能幫助您在應用程式和程式庫中充分利用這些功能!