混入
类混入是一种使用标准 JavaScript 在类之间共享代码的模式。与"拥有"(has-a)组合模式(如响应式控制器,类可以_拥有_一个控制器来添加行为)不同,混入实现的是"属于"(is-a)组合,即混入使类本身_成为_被共享行为的实例。
你可以使用混入来通过添加 API 或重写生命周期回调来自定义 Lit 组件。
混入可以被看作是"子类工厂",它们重写被应用的类并返回一个子类,该子类扩展了混入中的行为。 因为混入使用标准的 JavaScript 类表达式实现,所以它们可以使用子类化可用的所有惯用方式,例如添加新的字段/方法、重写现有的父类方法,以及使用 super。
为便于阅读,本页上的示例省略了混入函数的一些 TypeScript 类型。有关在 TypeScript 中正确使用混入类型的详情,请参阅 TypeScript 中的混入。
要定义一个混入,编写一个接收 superClass 参数的函数,并返回一个扩展它的新类,根据需要添加字段和方法:
const MyMixin = (superClass) => class extends superClass { /* 用于扩展 superClass 的类字段和方法 */}; 要应用一个混入,只需传递一个类来生成一个应用了混入的子类。最常见的情况是,用户在定义新类时直接将混入应用到基类上:
class MyElement extends MyMixin(LitElement) { /* 用户代码 */} 混入也可以用于创建具体的子类,用户可以像使用普通类一样扩展这些子类,而混入只是一个实现细节:
export const LitElementWithMixin = MyMixin(LitElement); import {LitElementWithMixin} from './lit-element-with-mixin.js';
class MyElement extends LitElementWithMixin { /* 用户代码 */} 因为类混入是一种标准的 JavaScript 模式,而非 Lit 特有的,所以社区中有大量关于利用混入进行代码复用的资料。以下是关于混入的一些好的参考资料:
- MDN 上的类混入
- Justin Fagnani 撰写的 Real Mixins with JavaScript Classes
- TypeScript 手册中的 Mixins。
- open-wc 的 Dedupe mixin library,包括关于混入使用何时可能导致重复的讨论,以及如何使用去重库来避免这种情况。
- Elix Web Components 库遵循的 Mixin conventions。虽然不是 Lit 特有的,但包含了关于在为 Web Components 定义混入时应用约定的深思熟虑的建议。
为 LitElement 创建混入
Permalink to "为 LitElement 创建混入"应用于 LitElement 的混入可以实现或重写任何标准的自定义元素生命周期回调(如 constructor() 或 connectedCallback()),以及任何响应式更新生命周期回调(如 render() 或 updated())。
例如,以下混入会在元素被创建、连接和更新时记录日志:
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`); }} 请注意,混入应始终对 LitElement 实现的标准自定义元素生命周期方法进行 super 调用。当重写响应式更新生命周期回调时,如果父类上已经存在该方法,调用 super 方法是一个好习惯(如上面所示使用可选链调用 super.updated?.())。
另请注意,混入可以选择通过选择何时进行 super 调用,来在标准生命周期回调的基础实现之前或之后执行工作。
下面示例中的混入为元素添加了一个 highlight 响应式属性和一个 renderHighlight() 方法,用户可以调用该方法来包裹一些内容。当设置 highlight 属性/特性时,被包裹的内容将以黄色样式显示。
请注意,在上面的示例中,混入的使用者需要从其 render() 方法中调用 renderHighlight() 方法,同时还需要注意将混入定义的 static styles 添加到子类样式中。混入与使用者之间这种契约的性质取决于混入的定义,应由混入作者记录说明。
TypeScript 中的混入
Permalink to "TypeScript 中的混入"在 TypeScript 中编写 LitElement 混入时,有几个细节需要注意。
类型化父类
Permalink to "类型化父类"你应该将 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 /* 见下方"类型化子类" */;} 上面的示例确保传递给混入的类继承自 LitElement,这样你的混入就可以依赖 Lit 提供的回调和其他 API。
类型化子类
Permalink to "类型化子类"虽然 TypeScript 对使用混入模式生成的子类的返回类型有基本的推断支持,但它有一个严重的限制:推断的类不能包含具有 private 或 protected 访问修饰符的成员。
因为 LitElement 本身具有私有和受保护的成员,默认情况下 TypeScript 在返回一个扩展 LitElement 的类时会报错:"Property '...' of exported class expression may not be private or protected."
有两种解决方法,它们都涉及对混入函数的返回类型进行类型转换来避免上述错误。
当混入不添加新的公共/受保护 API 时
Permalink to "当混入不添加新的公共/受保护 API 时"如果你的混入只重写 LitElement 的方法或属性,不添加任何自己的新 API,你可以简单地将生成的类转换为传入的父类类型 T:
export const MyMixin = <T extends Constructor<LitElement>>(superClass: T) => { class MyMixinClass extends superClass { connectedCallback() { super.connectedCallback(); this.doSomethingPrivate(); } private doSomethingPrivate() { /* 不需要成为接口的一部分 */ } }; // 将返回类型转换为传入的父类类型 return MyMixinClass as T;} 当混入添加了新的公共/受保护 API 时
Permalink to "当混入添加了新的公共/受保护 API 时"如果你的混入确实添加了新的受保护或公共 API,你需要用户能够在他们的类上使用这些 API,那么你需要将混入的接口与实现分开定义,并将返回类型转换为混入接口和父类类型的交叉类型:
// 定义混入的接口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() { /* ... */ } }; // 将返回类型转换为混入接口与父类类型的交叉类型 return MyMixinClass as Constructor<MyMixinInterface> & T;} 在混入中应用装饰器
Permalink to "在混入中应用装饰器"由于 TypeScript 类型系统的限制,装饰器(如 @property())必须应用于类声明语句,而不是类表达式。
在实践中,这意味着 TypeScript 中的混入需要声明一个类然后返回它,而不是直接从箭头函数返回类表达式。
支持的写法:
export const MyMixin = <T extends LitElementConstructor>(superClass: T) => { // ✅ 在函数体中定义类,然后返回它 class MyMixinClass extends superClass { property() mode = 'on'; /* ... */ }; return MyMixinClass;} 不支持的写法:
export const MyMixin = <T extends LitElementConstructor>(superClass: T) => // ❌ 使用箭头函数简写直接返回类表达式 class extends superClass { property() mode = 'on'; /* ... */ }