Mixins
類別 mixins 是一種使用標準 JavaScript 在類別之間共用程式碼的模式。與「has-a」組合模式(例如 響應式控制器)不同,在「has-a」組合模式中,類別可以擁有控制器來新增行為,mixins 實作「is-a」組合,其中 mixin 使類別本身成為正在共用的行為的實例。
您可以使用 mixins 來自訂 Lit 元件,方法是新增 API 或覆寫其生命週期回呼。
Mixin 基礎
「Mixin 基礎」的永久連結Mixins 可以被視為「子類別工廠」,它會覆寫套用到的類別並傳回子類別,並使用 mixin 中的行為擴充。由於 mixins 是使用標準 JavaScript 類別表達式實作,因此它們可以使用子類別的所有慣用語,例如新增新的欄位/方法、覆寫現有父類別方法,以及使用 super
。
為了方便閱讀,本頁上的範例省略了 mixin 函式的一些 TypeScript 型別。如需有關在 TypeScript 中正確設定 mixins 型別的詳細資訊,請參閱TypeScript 中的 Mixins。
若要定義 mixin,請撰寫一個採用 superClass
的函式,並傳回一個擴充該類別的新類別,並視需要新增欄位和方法
const MyMixin = (superClass) => class extends superClass {
/* class fields & methods to extend superClass with */
};
若要套用 mixin,只需傳遞一個類別,以產生套用 mixin 的子類別。最常見的情況是,使用者會在定義新類別時直接將 mixin 套用至基底類別
class MyElement extends MyMixin(LitElement) {
/* user code */
}
Mixins 也可用於建立具體子類別,使用者可以像擴充一般類別一樣擴充這些子類別,其中 mixin 是實作詳細資訊
export const LitElementWithMixin = MyMixin(LitElement);
import {LitElementWithMixin} from './lit-element-with-mixin.js';
class MyElement extends LitElementWithMixin {
/* user code */
}
由於類別 mixins 是一種標準 JavaScript 模式,而非 Lit 特有的模式,因此社群中有很多關於利用 mixins 來重複使用程式碼的資訊。如需有關 mixins 的更多閱讀資料,以下是一些不錯的參考資料
- MDN 上的類別 mixins
- Justin Fagnani 的 JavaScript 類別的真正 Mixins
- TypeScript 手冊中的 Mixins。
- open-wc 的重複資料刪除 mixin 函式庫,包括討論 mixin 的使用何時可能導致重複,以及如何使用重複資料刪除函式庫來避免重複。
- Elix Web 元件函式庫遵循的 Mixin 慣例。雖然不是 Lit 特有的,但包含關於在為 Web 元件定義 mixins 時套用慣例的周詳建議。
為 LitElement 建立 mixins
「為 LitElement 建立 mixins」的永久連結套用至 LitElement 的 Mixins 可以實作或覆寫任何標準的自訂元素生命週期回呼,例如 constructor()
或 connectedCallback()
,以及任何響應式更新生命週期回呼,例如 render()
或 updated()
。
例如,以下 mixin 會在建立、連線和更新元素時記錄
const LoggingMixin = (superClass) => class extends superClass {
constructor() {
super();
console.log(`${this.localName} was created`);
}
connectedCallback() {
super.connectedCallback();
console.log(`${this.localName} was connected`);
}
updated(changedProperties) {
super.updated?.(changedProperties);
console.log(`${this.localName} was updated`);
}
}
請注意,mixin 應始終對 LitElement
實作的標準自訂元素生命週期方法進行 super 呼叫。當覆寫響應式更新生命週期回呼時,如果父類別上已存在 super 方法,最好呼叫 super 方法(如上所示,對 super.updated?.()
進行選擇性鏈結呼叫)。
另請注意,mixin 可以選擇在標準生命週期回呼的基本實作之前或之後執行工作,方法是選擇何時進行 super 呼叫。
Mixins 也可以將響應式屬性、樣式和 API 新增至子類別化的元素。
以下範例中的 mixin 會將 highlight
響應式屬性新增至元素和 renderHighlight()
方法,使用者可以呼叫該方法來包裝一些內容。當設定 highlight
屬性/屬性時,包裝的內容會以黃色樣式顯示。
請注意,在上述範例中,mixin 的使用者應從其 render()
方法呼叫 renderHighlight()
方法,並注意將 mixin 定義的 static styles
新增至子類別樣式。mixin 與使用者之間此合約的性質取決於 mixin 定義,且應由 mixin 作者記錄。
TypeScript 中的 Mixins
「TypeScript 中的 Mixins」的永久連結在 TypeScript 中撰寫 LitElement
mixins 時,有幾個細節需要注意。
為父類別設定型別
「為父類別設定型別」的永久連結您應該將 superClass
引數限制為您預期使用者要擴充的類別型別(如果有的話)。這可以使用通用 Constructor
協助程式型別來完成,如下所示
import {LitElement} from 'lit';
type Constructor<T = {}> = new (...args: any[]) => T;
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => {
class MyMixinClass extends superClass {
/* ... */
};
return MyMixinClass as /* see "typing the subclass" below */;
}
上述範例確保傳遞給 mixin 的類別是從 LitElement
擴充而來,因此您的 mixin 可以依賴 Lit 提供的回呼和其他 API。
為子類別設定型別
「為子類別設定型別」的永久連結雖然 TypeScript 基本上支援推斷使用 mixin 模式產生的子類別的傳回型別,但它有一個嚴重的限制,即推斷的類別不得包含具有 private
或 protected
存取修飾詞的成員。
由於 LitElement
本身確實具有私有和受保護的成員,因此預設情況下,當傳回擴充 LitElement
的類別時,TypeScript 會顯示錯誤「匯出的類別表達式的屬性 '...' 不得為私有或受保護。」。
有兩種解決方法,都涉及將 mixin 函式的傳回型別轉換,以避免上述錯誤。
當 mixin 未新增新的 public/protected API 時
「當 mixin 未新增新的 public/protected API 時」的永久連結如果您的 mixin 僅覆寫 LitElement
方法或屬性,且未新增任何自己的新 API,您可以簡單地將產生的類別轉換為傳入的父類別型別 T
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => {
class MyMixinClass extends superClass {
connectedCallback() {
super.connectedCallback();
this.doSomethingPrivate();
}
private doSomethingPrivate() {
/* does not need to be part of the interface */
}
};
// Cast return type to the superClass type passed in
return MyMixinClass as T;
}
當 mixin 新增新的 public/protected API 時
「當 mixin 新增新的 public/protected API 時」的永久連結如果您的 mixin 確實新增了新的受保護或公用 API,而您需要使用者能夠在其類別上使用,您需要與實作分開定義 mixin 的介面,並將傳回型別轉換為 mixin 介面和父類別型別的交集
// Define the interface for the mixin
export declare class MyMixinInterface {
highlight: boolean;
protected renderHighlight(): unknown;
}
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => {
class MyMixinClass extends superClass {
@property() highlight = false;
protected renderHighlight() {
/* ... */
}
};
// Cast return type to your mixin's interface intersected with the superClass type
return MyMixinClass as Constructor<MyMixinInterface> & T;
}
在 mixins 中套用裝飾器
「在 mixins 中套用裝飾器」的永久連結由於 TypeScript 型別系統的限制,裝飾器(例如 @property()
)必須套用至類別宣告陳述式,而不是類別表達式。
實際上,這表示 TypeScript 中的 mixins 需要宣告一個類別,然後傳回它,而不是直接從箭頭函式傳回類別表達式。
支援
export const MyMixin = <T extends LitElementConstructor>(superClass: T) => {
// ✅ Defining a class in a function body, and then returning it
class MyMixinClass extends superClass {
@property()
mode = 'on';
/* ... */
};
return MyMixinClass;
}
不支援
export const MyMixin = <T extends LitElementConstructor>(superClass: T) =>
// ❌ Returning class expression directly using arrow-function shorthand
class extends superClass {
@property()
mode = 'on';
/* ... */
}