自訂指令
指令是函數,可以透過自訂模板表達式的渲染方式來擴展 Lit。指令非常有用且強大,因為它們可以是有狀態的、可以存取 DOM、可以在模板斷開連線和重新連線時收到通知,並且可以在渲染呼叫之外獨立更新表達式。
在您的模板中使用指令就像在模板表達式中呼叫函數一樣簡單
html`<div>
${fancyDirective('some text')}
</div>`
Lit 隨附許多內建指令,例如 repeat()
和 cache()
。使用者也可以編寫自己的自訂指令。
指令有兩種
- 簡單函數
- 基於類別的指令
簡單函數會傳回要渲染的值。它可以接受任意數量的引數,或者不接受任何引數。
export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x');
基於類別的指令可讓您執行簡單函數無法執行的操作。使用基於類別的指令來
- 直接存取渲染的 DOM (例如,新增、移除或重新排序渲染的 DOM 節點)。
- 在渲染之間保留狀態。
- 在渲染呼叫之外非同步更新 DOM。
- 在指令與 DOM 斷開連線時清除資源
本頁的其餘部分說明基於類別的指令。
建立基於類別的指令
“建立基於類別的指令”的永久連結若要建立基於類別的指令
- 將指令實作為一個類別,該類別會擴展
Directive
類別。 - 將您的類別傳遞給
directive()
工廠,以建立可用於 Lit 模板表達式的指令函數。
import {Directive, directive} from 'lit/directive.js';
// Define directive
class HelloDirective extends Directive {
render() {
return `Hello!`;
}
}
// Create the directive function
const hello = directive(HelloDirective);
// Use directive
const template = html`<div>${hello()}</div>`;
評估此模板時,指令函數 (hello()
) 會傳回 DirectiveResult
物件,該物件會指示 Lit 建立或更新指令類別 (HelloDirective
) 的執行個體。然後,Lit 會呼叫指令執行個體的方法來執行其更新邏輯。
有些指令需要非同步更新 DOM,在正常更新週期之外。若要建立非同步指令,請擴展 AsyncDirective
基底類別,而不是 Directive
。如需詳細資訊,請參閱 非同步指令。
基於類別的指令的生命週期
“基於類別的指令的生命週期”的永久連結指令類別具有一些內建的生命週期方法
- 類別建構函式,用於一次性初始化。
render()
,用於宣告式渲染。update()
,用於命令式 DOM 存取。
您必須為所有指令實作 render()
回呼。實作 update()
是選用的。update()
的預設實作會呼叫 render()
並傳回其值。
非同步指令可以在正常更新週期之外更新 DOM,使用一些額外的生命週期回呼。如需詳細資訊,請參閱 非同步指令。
一次性設定:constructor()
“一次性設定:constructor()”的永久連結當 Lit 第一次在表達式中遇到 DirectiveResult
時,它會建構對應指令類別的執行個體 (導致執行指令的建構函式和任何類別欄位初始設定式)
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo: PartInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
只要在每次渲染時在相同的表達式中使用相同的指令函數,就會重複使用先前的執行個體,因此執行個體的狀態會在渲染之間持續存在。
建構函式會接收單一 PartInfo
物件,其中提供有關使用指令的表達式的中繼資料。這對於在指令僅設計用於特定類型的表達式時提供錯誤檢查非常有用 (請參閱 將指令限制為一種表達式類型)。
宣告式渲染:render()
“宣告式渲染:render()”的永久連結render()
方法應傳回要渲染到 DOM 中的值。它可以傳回任何可渲染的值,包括另一個 DirectiveResult
。
除了參照指令執行個體上的狀態之外,render()
方法還可以接受傳遞到指令函數中的任意引數
const template = html`<div>${myDirective(name, rank)}</div>`
為 render()
方法定義的參數會決定指令函數的簽章
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value: number, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
命令式 DOM 存取:update()
“命令式 DOM 存取:update()”的永久連結在更進階的使用案例中,您的指令可能需要存取基礎 DOM 並以命令方式讀取或變更它。您可以透過覆寫 update()
回呼來達成此目的。
update()
回呼會接收兩個引數
- 具有直接管理與表達式相關聯之 DOM 的 API 的
Part
物件。 - 包含
render()
引數的陣列。
您的 update()
方法應傳回 Lit 可以渲染的內容,如果不需要重新渲染,則傳回特殊值 noChange
。update()
回呼相當有彈性,但典型用法包括
- 從 DOM 讀取資料,並使用它來產生要渲染的值。
- 使用
Part
物件上的element
或parentNode
參照來以命令方式更新 DOM。在此情況下,update()
通常會傳回noChange
,表示 Lit 不需要採取任何進一步的動作來渲染指令。
Part
“Part”的永久連結每個表達式位置都有其自己的特定 Part
物件
ChildPart
用於 HTML 子位置中的表達式。AttributePart
用於 HTML 屬性值位置中的表達式。BooleanAttributePart
用於布林值屬性值 (名稱前置詞為?
) 中的表達式。EventPart
用於事件接聽器位置 (名稱前置詞為@
) 中的表達式。PropertyPart
用於屬性值位置 (名稱前置詞為.
) 中的表達式。ElementPart
用於元素標籤上的表達式。
除了 PartInfo
中包含的特定部分中繼資料之外,所有 Part
類型都提供對與表達式相關聯之 DOM element
(或 ChildPart
的 parentNode
) 的存取,該 DOM 可以在 update()
中直接存取。例如
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part: ChildPart) {
this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part) {
this.attributeNames = part.parentNode.getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
此外,directive-helpers.js
模組包含許多對 Part
物件起作用的協助程式函數,可用於在指令的 ChildPart
中動態建立、插入和移動部分。
從 update() 呼叫 render()
“從 update() 呼叫 render()”的永久連結update()
的預設實作只會呼叫並傳回 render()
的值。如果您覆寫 update()
並且仍然想要呼叫 render()
來產生值,您需要明確呼叫 render()
。
render()
引數會以陣列的形式傳遞到 update()
中。您可以這樣將引數傳遞給 render()
class MyDirective extends Directive {
update(part: Part, [fish, bananas]: DirectiveParameters<this>) {
// ...
return this.render(fish, bananas);
}
render(fish: number, bananas: number) { ... }
}
class MyDirective extends Directive {
update(part, [fish, bananas]) {
// ...
return this.render(fish, bananas);
}
render(fish, bananas) { ... }
}
update() 和 render() 之間的差異
“update() 和 render() 之間的差異”的永久連結雖然 update()
回呼比 render()
回呼更強大,但有一個重要的區別:當使用 @lit-labs/ssr
套件進行伺服器端渲染 (SSR) 時,只有在伺服器上呼叫 render()
方法。為了與 SSR 相容,指令應該從 render()
傳回值,並且僅將 update()
用於需要存取 DOM 的邏輯。
表示沒有變更
“表示沒有變更”的永久連結有時候,指令可能沒有新的內容供 Lit 渲染。您可以透過從 update()
或 render()
方法傳回 noChange
來表示此情況。這與傳回 undefined
不同,後者會導致 Lit 清除與指令相關聯的 Part
。傳回 noChange
會讓先前渲染的值保留在原處。
傳回 noChange
有幾個常見原因
- 根據輸入值,沒有新內容要渲染。
update()
方法以命令方式更新了 DOM。- 在非同步指令中,對
update()
或render()
的呼叫可能會傳回noChange
,因為尚未有內容要渲染。
例如,指令可以追蹤傳遞給它的先前值,並執行自己的髒檢查,以判斷是否需要更新指令的輸出。update()
或 render()
方法可以傳回 noChange
,表示不需要重新渲染指令的輸出。
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
a?: string;
b?: string;
render(a: string, b: string) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// Expensive & fancy text diffing algorithm
return calculateDiff(a, b);
}
return noChange;
}
}
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
render(a, b) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// Expensive & fancy text diffing algorithm
return calculateDiff(a, b);
}
return noChange;
}
}
將指令限制為一種表達式類型
“將指令限制為一種表達式類型”的永久連結有些指令僅在一個內容中才有用,例如屬性表達式或子表達式。如果放置在錯誤的內容中,指令應擲回適當的錯誤。
例如,classMap
指令會驗證它是否僅在 AttributePart
中使用,並且僅適用於 class
屬性
class ClassMap extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
class ClassMap extends Directive {
constructor(partInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
非同步指令
“非同步指令”的永久連結先前的範例指示是同步的:它們從其 render()
/update()
生命周期回呼中同步傳回值,因此它們的結果會在元件的 update()
回呼期間寫入 DOM。
有時,您會希望指示能夠非同步更新 DOM,例如,當它依賴於非同步事件(如網路請求)時。
若要非同步更新指示的結果,指示需要擴展 AsyncDirective
基礎類別,該類別提供 setValue()
API。setValue()
允許指示在範本的正常 update
/render
週期之外,將新值「推送」到其範本表達式中。
以下是一個簡單的非同步指示範例,它會渲染 Promise 值
class ResolvePromise extends AsyncDirective {
render(promise: Promise<unknown>) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
class ResolvePromise extends AsyncDirective {
render(promise) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
在這裡,渲染的範本顯示「等待 Promise 解析」,然後顯示 Promise 的解析值,無論何時解析。
非同步指示通常需要訂閱外部資源。為防止記憶體洩漏,非同步指示應在不再使用指示實例時取消訂閱或處置資源。為此,AsyncDirective
提供了以下額外的生命週期回呼和 API
disconnected()
:當不再使用指示時呼叫。指示實例在以下三種情況下會斷開連線- 當指示所在的 DOM 樹從 DOM 中移除時
- 當指示的主機元素斷開連線時
- 當產生指示的表達式不再解析為相同的指示時。
在指示收到
disconnected
回呼後,應釋放它在update
或render
期間可能訂閱的所有資源,以防止記憶體洩漏。reconnected()
:當先前斷開連線的指示恢復使用時呼叫。由於 DOM 子樹可以暫時斷開連線,然後稍後重新連線,因此斷開連線的指示可能需要對重新連線做出反應。這方面的範例包括當 DOM 被移除並快取以供稍後使用,或者當主機元素移動導致斷開連線和重新連線時。reconnected()
回呼應始終與disconnected()
一起實作,以便將斷開連線的指示恢復到其工作狀態。isConnected
:反映指示的目前連線狀態。
請注意,如果重新渲染其包含的樹狀結構,則 AsyncDirective
可能會在斷開連線時繼續接收更新。因此,update
和/或 render
應始終檢查 this.isConnected
標誌,然後再訂閱任何長期保留的資源,以防止記憶體洩漏。
以下是一個訂閱 Observable
並適當處理斷開連線和重新連線的指示範例
class ObserveDirective extends AsyncDirective {
observable: Observable<unknown> | undefined;
unsubscribe: (() => void) | undefined;
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable: Observable<unknown>) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable: Observable<unknown>) {
this.unsubscribe = observable.subscribe((v: unknown) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe!();
}
// If the subtree the directive is in was disconnected and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable!);
}
}
export const observe = directive(ObserveDirective);
class ObserveDirective extends AsyncDirective {
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable) {
this.unsubscribe = observable.subscribe((v) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe();
}
// If the subtree the directive is in was disconneted and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable);
}
}
export const observe = directive(ObserveDirective);