過去一年,Angular 推出了許多新功能,例如水合和延遲檢視畫面,協助開發人員改善Core Web Vitals,確保使用者享有絕佳體驗。我們也正在研究其他建構於此功能的伺服器端算繪相關功能,例如串流和部分重新整理。
不過,有一種模式可能會導致應用程式或程式庫無法充分利用所有新功能和即將推出的功能:手動操作底層 DOM 結構。Angular 要求 DOM 的結構在伺服器序列化元件時保持一致,直到在瀏覽器上重新注入為止。在充填前使用 ElementRef
、Renderer2
或 DOM API 手動新增、移動或移除 DOM 中的節點,可能會導致不一致性,導致這些功能無法運作。
不過,並非所有手動 DOM 操控和存取作業都會導致問題,有時甚至是必要的。安全使用 DOM 的關鍵,在於盡可能減少使用 DOM 的需求,並盡可能延後使用 DOM。以下規範說明如何達成這項目標,並建立真正通用且可因應未來需求的 Angular 元件,充分發揮 Angular 所有新功能和即將推出的功能。
避免手動操控 DOM
要避免手動 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
可接受 phase
值 EarlyRead
、Read
或 Write
。在寫入 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 伺服器端算繪功能即將推出許多令人期待的新功能,目的是讓您更輕鬆地為使用者提供優質體驗。希望上述提示能幫助您在應用程式和程式庫中充分利用這些功能!