面向 Polymer 用户的 Lit

— 更新于
Photo of Arthur Evans
Arthur Evans

Lit 是 Polymer 库的继任者。如果您有一个使用 Polymer 构建的项目,并希望将其迁移到 Lit,或者您熟悉 Polymer 并想知道它与 Lit 的比较,那么本文档适合您。

本文档简要概述了 Lit 与 Polymer 的关系,并提供了一个食谱,展示了常见的 Polymer 代码如何转换为 Lit。

Polymer 是最早构建 Web 组件的库之一。Lit 是 Polymer 的继任者,由同一个团队构建,并具有许多相同目标。这两个项目拥有许多共同目标,但 Lit 利用了在 Polymer 开发过程中汲取的经验教训。

由于 Polymer 是 Lit 的前身,这两个库之间存在许多相似之处。这两个库都简化了构建像内置 HTML 元素一样工作的组件,并且都具有声明式 HTML 模板。

Lit 在几个方面与 Polymer 不同

  • Lit 的渲染默认情况下是异步的,并且是批处理的。除了少数例外,所有 Polymer 更新都是同步的。

  • Lit 公开了一个更新生命周期,它提供了一个强大的机制来观察对属性的更改,并从属性中计算派生值。Polymer 具有声明式观察器和计算属性,但很难预测观察器的运行顺序。

  • Lit 侧重于以 JavaScript 为首的创作,使用原生 JavaScript 模块。Polymer 最初侧重于以 HTML 为首的创作,这得益于 HTML Imports 规范,该规范现已从 Web 平台中移除。

  • Lit 表达式使用标准 JavaScript。Polymer 在其绑定中使用有限的特定于领域的语言。由于 Lit 使用标准 JavaScript,因此您还可以使用 JavaScript 在表达式中进行控制流(条件模板和重复模板),而 Polymer 使用专门的辅助元素。

  • Lit 旨在通过单向数据流实现简单易懂的心理模型。Polymer 支持双向数据绑定和观察器,这在简单的项目中可能非常有用,但随着项目变得越来越复杂,往往会使数据流更难理解。

如果您计划从 Polymer 迁移到 Lit,您不必一次性完成所有操作。Polymer 和 Lit 可以协同工作,因此您可以一次迁移一个组件。

在开始使用 Lit 之前,您需要执行一些操作来更新您的项目

  • 将您的项目更新到 Polymer 3。

  • 确保您的项目工具支持更新的 JavaScript 功能。

  • 移除双向绑定。

Polymer 2.x 及更早版本使用 HTML Imports,该功能现已从 Web 平台中移除。Polymer 3 和 Lit 都作为 JavaScript 模块分发,这意味着它们可以很好地协同工作,并可以利用各种现代 Web 工具。

在大多数情况下,大部分迁移过程可以使用 Polymer 模块化工具自动完成。有关更多信息,请参阅 Polymer 3.0 升级指南

Polymer 使用了 ECMAScript 2015 版 JavaScript 规范中的功能。如果您从 Polymer 启动器套件之一开始,您的工具链可能不支持更新的 JavaScript。

Lit 使用了 ECMAScript 2019 中的功能(并且一些 Lit 示例代码可能包含更新的语言功能)。如果您使用的是不支持这些更新的 JavaScript 功能的工具,则需要更新这些工具。例如

  • 公共实例字段和公共静态字段。这些在示例代码中被广泛使用

  • 异步和等待,可用于简化基于 Promise 的代码。例如

有关 Lit 语言要求的更多信息,请参阅 要求

Polymer 的双向绑定有效地将宿主属性与子属性紧密耦合。这种紧密耦合会产生许多问题,因此团队选择不在 Lit 中添加双向绑定。

迁移到 Lit 之前移除双向绑定将减少迁移的复杂性,并允许您在开始迁移之前在 Polymer 中测试您的应用程序,而无需双向绑定。

如果您的应用程序未使用双向绑定,则可以跳过本节。

对于双向绑定,Polymer 使用其自身的协议,该协议具有三个主要组件

  • 从宿主到子的绑定。

  • 处理子元素属性更改事件的事件监听器。

  • 子元素在属性更改时自动触发更改事件的工具(notify: true)。

最后一点问题最大。组件会对属性的任何更改触发更改事件,并且每个更改事件都是同步处理的。在整个应用程序中广泛使用双向绑定会导致很难理解数据流以及组件更新自身的顺序。

理想情况下,事件是一个离散的信号,用于传达显式更改,而该更改不能轻易观察到。在设置属性的副作用时发送事件(如 Polymer 所做的那样)会使通信可能变得冗余和隐式。这种隐式行为,尤其是,会使数据流难以理解。

为了概括 自定义元素最佳实践 指南,组件应该在以下情况下触发事件

  • 当元素状态由于用户交互而发生更改时,例如单击按钮或在组件内部编辑文本字段。

  • 当组件内部发生一些内部更改时,例如计时器到期或动画完成。

理想情况下,组件应该触发语义事件,这些事件描述了发生了什么变化,而不是让低级 UI 事件冒泡出来。例如,允许用户更新个人资料数据的表单可能会在用户单击完成按钮时触发profile-updated事件。profile-updated事件与父级相关:单击事件无关。

有很多方法可以替换双向绑定。如果您只是想将现有 Polymer 应用程序迁移到 Lit,您可能只想用执行类似功能的代码来替换双向绑定

  • 移除自动属性更改事件(notify: true)。

  • 用更具意图的更改事件替换自动属性更改事件。

  • 用单向绑定和事件处理程序替换双向绑定注释。(此步骤是可选的,但会简化向 Lit 的过渡。)

例如,如果子元素由于用户交互而更新通知属性,请移除notify: true并手动基于用户交互事件触发事件更改事件。

考虑以下 Polymer 代码

此组件对input元素使用双向绑定,并具有通知属性,因此父元素可以使用name属性与之进行双向绑定。对输入的双向绑定很有意义,因为绑定仅在input事件触发时设置。但是,通过添加input事件的事件监听器,您可以消除通知属性

此代码显式监听输入事件并触发name-changed事件。此代码将与预期 Polymer 双向绑定协议的父元素一起使用,因此您可以一次更新一个组件。

此代码不会在父元素设置name属性时触发属性更改事件,这是一件好事。并且您可以将此代码相当直接地迁移到 Lit。

使用这样的事件处理程序,您还可以向子组件添加逻辑,例如,仅在用户停止键入一段时间后才触发name-changed事件。

单向数据流。

另一个替代方案是用单向数据流模式替换双向绑定,使用 Redux 等状态容器。各个组件可以订阅状态更新并分派操作以更新状态。我们建议将此用于新开发,但这可能需要更多工作,特别是如果您已经有一个基于双向绑定的应用程序。

本节展示了 Polymer 代码如何处理常见任务,并展示了等效的 Lit 代码。如果您已经熟悉 Polymer 并想学习 Lit,或者如果您正在将现有项目从 Polymer 迁移到 Lit,这将很有帮助。

本节假定您使用的是 Polymer 3.0。如果您正在将项目从 Polymer 迁移到 Lit,您将需要首先迁移到 Polymer 3.0

有关迁移现有项目的更多特定信息,请参阅 从 Polymer 迁移到 Lit

JavaScript 还是 TypeScript?

Lit 与两者都很匹配。大多数 Lit 示例使用可切换代码样本小部件显示,因此您可以选择 TypeScript 或 JavaScript 语法。

Polymer 和 Lit 都基于 Web 组件,因此在两个库中定义组件看起来非常相似。在最简单的情况下,唯一的区别是基类。

Polymer

Lit

Lit 提供了一组可以改善您的开发体验的装饰器,例如上一节 TypeScript 示例中显示的customElement。请注意,本网站上的 TypeScript 示例包含装饰器,而 JavaScript 示例省略了装饰器,但您实际上可以使用装饰器的 JavaScript 或使用不带装饰器的 TypeScript。这取决于您。在 JavaScript 中使用装饰器需要像 Babel 这样的编译器。(由于您已经需要 TypeScript 的编译器,因此您只需配置它来处理装饰器即可。)有关更多信息,请参阅 装饰器

Polymer 3 和 Lit 都提供了用于定义模板的html标记函数。

Polymer

Lit

请注意,Lit 的 html 与 Polymer 的 html 不同。它们具有相同的基本目的,但工作方式不同。Polymer 的 html 在元素初始化期间被调用一次。Lit 的 html 通常在每次更新期间被调用。设置工作在每个模板字面量字符串中执行一次,因此后续对 Lit 的 html 函数的增量更新调用非常快。

有关 Lit 模板的更多信息,请参见 模板概述

Polymer 组件通常在模板中直接包含样式。

在 Lit 中,你通常使用 css 标记函数在静态的 styles 字段中提供样式。

在模板中直接添加样式标签(就像你在 Polymer 中做的那样)也是支持的

使用样式标签的性能可能略低于静态的 styles 字段,因为样式是针对每个实例评估一次,而不是针对每个类评估一次。

有关更多信息,请参见 样式

Polymer 具有数据绑定,而 Lit 在其模板中具有表达式。Lit 表达式是标准的 JavaScript 表达式,它们绑定到模板中的特定位置。因此,Lit 表达式可以执行几乎所有你可以在 Polymer 数据绑定中执行的操作,以及许多你在 Polymer 中难以执行的操作。

双向绑定。

Lit 团队有意选择不实现双向数据绑定。虽然此功能似乎简化了一些常见需求,但实际上它使得难以推理数据流以及组件更新自身顺序。我们建议在迁移到 Lit 之前删除双向绑定。有关更多信息,请参见 删除双向绑定

Polymer

Lit

如你所见,主要区别在于用带标记的模板字面量的表达式语法替换了围绕 Polymer 绑定的双括号

[[expression]]

用表达式语法替换为带标记的模板字面量

${expression}

另外,请注意,Lit 表达式使用 this.name 而不是 name。Polymer 绑定仅允许绑定注释中包含某些内容,例如属性名称或路径(例如 user.idshapes[4].type)。属性名称或路径是在当前 绑定范围内评估的。

你可以在 Lit 中使用任何标准 JavaScript 表达式,并应用标准 JavaScript 范围。例如,你可以访问在 render() 方法内部创建的局部变量,或使用 this 访问实例变量。

与 Polymer 一样,Lit 支持使用表达式设置属性、属性和事件处理程序。Lit 使用略微不同的语法,使用前缀而不是后缀。下表总结了 Polymer 和 Lit 绑定语法之间的差异

类型PolymerLit
属性property-name=[[value]].propertyName=${value}
属性attribute-name$=[[value]]attribute-name=${value}
布尔属性attribute-name?=[[value]]?attribute-name=${value}
事件on-event-name$=[[handler]]@event-name=${handler}

注意

  • 属性表达式。Lit 使用字面量属性名称,并在其前面加上句点。Polymer 使用相应的 属性名称

  • 事件处理程序。在 Lit 中,处理程序可以是方法,如 ${this.clickHandler} 或箭头函数。使用箭头函数,你可以闭包其他数据或调用具有不同签名的函数。例如

有关更多信息,请参见 表达式

一般来说,我们建议在将 Polymer 项目迁移到 Lit 之前删除双向绑定。有关更多信息,请参见 删除双向绑定

如果你已将双向绑定替换为单向绑定和事件监听器,则应该能够将它们相当直接地迁移到 Lit。

如果你正在编写一个 Lit 组件来替换以前使用双向绑定与 Polymer 子组件进行通信的父组件,请添加一个属性表达式来设置属性,并添加一个事件监听器来处理 property-changed 事件。

Polymer

Lit

Polymer 使用 dom-if 帮助器元素支持条件。

在 Lit 中,你可以使用 JavaScript 条件表达式。条件运算符(或三元运算符)非常有效

与 Polymer dom-if 不同,条件运算符允许你为真假条件提供内容,虽然你也可以返回空字符串以不呈现任何内容,如示例所示。

有关更多信息,请参见 条件

默认情况下,Polymer 的 dom-if 的行为与 Lit 条件略有不同。当条件从真值变为假值时,dom-if 只会隐藏条件 DOM,而不是从 DOM 树中删除它。当条件再次变为真值时,这可能会节省一些资源。

将 Polymer dom-if 迁移到 Lit 时,你有几种选择

  • 使用简单的 JavaScript 条件。当条件变为假值时,Lit 会删除并丢弃条件 DOM。如果你将 restamp 属性设置为 truedom-if 会执行相同的操作。

  • 使用标准的 hidden 属性来隐藏内容,而不从页面中删除它。

    这非常轻量级。但是,即使条件为假,DOM 也会在第一次渲染时创建。

  • 将条件包装在 cache 指令中,以避免在条件发生变化时丢弃和重新创建 DOM。

在大多数情况下,简单的条件非常有效。如果条件 DOM 很大很复杂,并且你在条件切换为真值时观察到重新创建 DOM 的延迟,则可以使用 Lit 的 cache 指令来保留条件 DOM。使用 cache 时,DOM 仍然从树中删除,但会缓存在内存中,这可以在条件发生变化时节省资源。

由于这在条件为假值时不会呈现任何内容,因此你可以使用它来避免在初始页面加载时创建复杂的 DOM 部分。

Polymer 使用 dom-repeat 帮助器来重复模板。

dom-repeat 内部的模板可以访问一组有限的属性:主机元素上的属性,以及 dom-repeat 添加的 itemindex 属性。

与条件一样,Lit 可以使用 JavaScript 处理重复,方法是让表达式返回一个值数组。Lit 的 map 指令的工作方式类似于 map() 数组方法,除了它接受其他类型的可迭代对象,如集合或生成器。

虽然 Polymer 绑定只能访问某些属性,但 Lit 表达式可以访问 JavaScript 范围内可用的任何内容。

你也可以在 render() 方法中生成数组

有关更多信息,请参见 列表,或尝试我们关于 使用列表 的交互式教程。

处理来自重复模板的事件

有多种方法可以将事件监听器添加到重复的元素

  • 如果事件冒泡,则可以使用事件委托,将单个监听器添加到父元素。

  • 如果事件不冒泡,则可以将监听器添加到每个重复的元素。

有关使用两种技术的示例,请参见“事件”部分中的 监听从重复模板触发的事件

处理来自重复模板生成的元素的事件时,你通常需要识别触发事件的元素以及生成该元素的数据。

Polymer 通过 将数据添加到事件对象来帮助解决这个问题。

Lit 不会添加这些额外的数据,但你可以为每个重复的元素附加一个唯一的值,以便于引用

将监听器添加到单个项目时,你还可以使用箭头函数将数据直接传递给事件处理程序

Lit 的响应式属性与 Polymer 的声明属性非常匹配。响应式属性支持与声明属性相同的许多功能,包括在属性和属性之间同步值。

与 Polymer 一样,Lit 允许你使用静态的 properties 字段来配置属性。

Lit 还支持使用 @property 装饰器来声明响应式属性。有关更多信息,请参见 关于装饰器

Polymer

Lit

Polymer 和 Lit 都在声明属性时支持许多选项。以下列表显示了 Polymer 选项及其 Lit 等效项。

type

Lit 的 type 选项具有相同的用途。

value

Lit 不支持像这样设置属性的值。而是要在构造函数中设置默认值。如果使用 @property 装饰器,你也可以使用类字段初始化器。

reflectToAttribute

在 Lit 中,此选项已缩短为 reflect

readOnly

Lit 不包含对只读响应式属性的任何内置支持。如果你需要使计算属性成为组件公共 API 的一部分,则可以添加一个没有设置器的 getter。

notify

此功能用于支持双向绑定。它没有在 Lit 中实现,因为存在 双向绑定问题中描述的问题。

Lit 组件可以使用本机 Web API(例如 dispatchEvent)来响应用户输入或内部状态更改时触发事件。

computed

Lit 不支持声明式计算属性。请参阅计算属性以了解替代方案。

observer

Lit 不直接支持观察者。相反,它在生命周期中提供了一些覆盖点,你可以在这些点中根据已更改的任何属性采取行动。

请注意,Polymer 示例使用 getter——static get properties()——而 Lit JavaScript 示例使用类字段——static properties。这两种形式的工作方式相同。发布 Polymer 3 时,对类字段的支持并不广泛,因此 Polymer 示例代码使用 getter。

如果你将 Lit 组件添加到现有的 Polymer 应用程序中,你的工具链可能不支持类字段格式。在这种情况下,你可以使用静态 getter 代替。

与 Polymer 一样,当属性发生更改时,Lit 会执行脏检查以避免执行不必要的操作。如果你的属性包含对象或数组,这可能会导致问题。如果你修改对象或数组,Lit 不会检测到更改。

在大多数情况下,避免这些问题的最佳方法是使用不可变数据模式,以便你始终分配新的对象或数组值,而不是修改现有对象或数组。对于 Polymer 通常也是如此。

Polymer 包含用于可观察地 设置子属性修改数组的 API,但它们的使用有些困难。如果你正在使用这些 API,你可能需要迁移到不可变数据模式。

有关更多信息,请参见 修改对象和数组属性

只读属性在 Polymer 中并不是一个非常常用的功能,而且如果需要,它们也不难实现,所以没有包含在 Lit API 中。

要将内部状态值作为组件公开 API 的一部分公开,您可以添加一个没有 setter 的 getter。您可能还希望在状态更改时触发事件(例如,从网络加载资源的组件可能具有“加载”状态并在该状态更改时触发事件)。

Polymer 的只读属性包含一个隐藏的 setter。如果这对您的组件有意义,您可以为您的属性添加一个私有 setter。

请注意,如果属性未包含在组件的模板中,则无需声明它(使用 @propertystatic properties)或调用 requestUpdate()

Lit 提供了许多可覆盖的生命周期方法,这些方法在响应式属性更改时调用。使用这些方法来实现将放在 Polymer 中的计算属性或观察者中的逻辑。

以下列表总结了如何迁移不同类型的计算属性和观察者。

计算属性

  • 对于在模板中临时使用的值,请在 render() 中将值计算为局部变量,并在返回之前在模板中使用它们。

  • 对于需要持久存储或计算成本高的值,请在 willUpdate() 中进行计算。

  • 使用 changedProperties.has() 方法仅在依赖项更改时计算,以避免每次更新都进行昂贵的重新计算。

观察者

  • 如果观察者需要根据属性更改直接作用于 DOM,请使用 updated()。这在 render() 回调之后调用。

  • 否则,请使用 willUpdate()

  • 在任何一种情况下,请使用 changedProperties.has() 来了解属性何时发生更改。

基于路径的观察者

  • 这些是观察像 foo.barfoo.* 这样的路径的复杂观察者。

  • 此功能非常特定于 Polymer 的数据系统,在 Lit 中没有等效项。

  • 我们建议使用不可变模式作为一种更具互操作性的方式来传达深度属性更改。有关更多信息,请参阅 修改对象和数组属性

如果需要计算仅用于渲染的瞬态值,则可以直接在 render() 方法中计算它们。

Polymer

Lit

此外,由于 Lit 允许您在模板中使用任何 JavaScript 表达式,因此 Polymer 的 计算绑定 可以用内联表达式替换,包括函数调用。

Polymer

Lit

由于 Lit 表达式是纯 JavaScript,因此您需要在表达式中使用 this 来访问实例属性或方法。

willUpdate() 回调是根据其他属性值计算属性的理想位置。willUpdate() 接收一个已更改属性值的映射,因此您可以处理当前更改。

使用 willUpdate() 可以根据已更改属性的完整集合来选择要执行的操作。这避免了多个观察者或计算属性以不可预测的方式交互的问题。

Mixins 是用于将可重用功能打包以便在 Polymer 组件中使用的几种方法之一。如果您要将 Polymer mixin 移植到 Lit,则有几个选项。

  • 独立函数。由于 Polymer 的数据绑定只能访问实例成员,因此人们经常创建 mixin 只是为了在数据绑定中提供一个函数。在 Lit 中不需要这样做,因为您可以在模板中使用任何 JavaScript 表达式。与其使用 mixin,不如从另一个模块导入函数并直接在模板中使用它。

  • Lit mixins。Lit mixins 的工作原理与 Polymer mixins 非常相似,因此许多 Polymer mixins 可以重新实现为 Lit mixins。有关更多信息,请参阅 Mixins

  • 响应式控制器。响应式控制器是打包可重用功能的另一种方式。有关 mixins 与响应式控制器的比较,请参阅 控制器和 mixins

Lit 组件与 Polymer 组件具有相同的一组标准 Web 组件生命周期回调。

此外,Lit 组件有一组回调,可用于自定义 响应式更新周期

如果您在 Polymer 元素中使用 ready() 回调,您可能可以使用 Lit 的 firstUpdated() 回调来实现相同的目的。firstUpdated() 回调在组件的 DOM 第一次渲染后调用。例如,如果您想在渲染的 DOM 中聚焦一个元素,则可以使用它。

有关更多信息,请参阅 完成更新