混入
类混入(Class mixins)是使用标准 JavaScript 在类之间共享代码的模式。与响应式控制器这样的"has-a"(有一个)组合模式不同,在"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 上的 Class mixins
- Justin Fagnani 的 Real Mixins with JavaScript Classes
- TypeScript 手册中的 Mixins
- open-wc 的 Dedupe mixin library,包括关于混入使用何时可能导致重复的讨论,以及如何使用去重库来避免这种情况
- Elix web 组件库遵循的 Mixin conventions。虽然不是 Lit 特有的,但包含了关于为 web 组件定义混入时应用约定的深思熟虑的建议
为 LitElement 创建混入
Permalink to "为 LitElement 创建混入"应用于 LitElement 的混入可以实现或覆盖任何标准的自定义元素生命周期回调,如 constructor()
或 connectedCallback()
,以及任何响应式更新生命周期回调,如 render()
或 updated()
。
例如,以下混入会在元素被创建、连接和更新时记录日志:
const LoggingMixin = (superClass) => class extends superClass {
constructor() {
super();
console.log(`${this.localName} 已创建`);
}
connectedCallback() {
super.connectedCallback();
console.log(`${this.localName} 已连接`);
}
updated(changedProperties) {
super.updated?.(changedProperties);
console.log(`${this.localName} 已更新`);
}
}
注意,混入应该始终对 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,你需要单独定义混入的接口,并将返回类型转换为你的混入接口和超类类型的交集:
// 定义混入的接口
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';
/* ... */
}