你正在查看 Lit 的旧版本文档。点击 这里查看最新版本。

混入

类混入是一种使用标准 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 特有的,所以社区中有大量关于利用混入进行代码复用的资料。以下是关于混入的一些好的参考资料:

应用于 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 调用,来在标准生命周期回调的基础实现之前或之后执行工作。

混入还可以向子类化的元素添加响应式属性样式和 API。

下面示例中的混入为元素添加了一个 highlight 响应式属性和一个 renderHighlight() 方法,用户可以调用该方法来包裹一些内容。当设置 highlight 属性/特性时,被包裹的内容将以黄色样式显示。

请注意,在上面的示例中,混入的使用者需要从其 render() 方法中调用 renderHighlight() 方法,同时还需要注意将混入定义的 static styles 添加到子类样式中。混入与使用者之间这种契约的性质取决于混入的定义,应由混入作者记录说明。

在 TypeScript 中编写 LitElement 混入时,有几个细节需要注意。

你应该将 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。

虽然 TypeScript 对使用混入模式生成的子类的返回类型有基本的推断支持,但它有一个严重的限制:推断的类不能包含具有 privateprotected 访问修饰符的成员。

因为 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;
}

由于 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';
/* ... */
}