1. 简介
上次更新时间:2021 年 8 月 10 日
Web 组件
Web 组件是一组 Web 平台 API,可让您创建新的自定义、可重复使用、封装的 HTML 标记,以便在网页和 Web 应用中使用。基于 Web 组件标准构建的自定义组件和小部件可在各种新式浏览器中运行,并且可以与任何适用于 HTML 的 JavaScript 库或框架搭配使用。
什么是 Lit
Lit 是一个简单的库,用于构建快速、轻量且适用于任何框架(或完全不使用框架)的 Web 组件。借助 Lit,您可以构建可共享的组件、应用、设计系统等。
Lit 提供了一些 API 来简化常见的 Web 组件任务,例如管理属性、特性和渲染。
学习内容
- 什么是 Web 组件
- Web 组件的概念
- 如何构建 Web 组件
- 什么是 lit-html 和 LitElement
- Lit 在 Web 组件的基础上做了哪些工作
构建内容
- 一个 vanilla“我喜欢 / 不喜欢”Web 组件
- 基于 Lit 的表示赞成 / 反对的 Web 组件
所需条件
- 任何更新后的新式浏览器(Chrome、Safari、Firefox、Chromium Edge)。Web 组件可在所有新式浏览器中使用,并且 Microsoft Internet Explorer 11 和非 Chromium Microsoft Edge 提供 Polyfill。
- 了解 HTML、CSS、JavaScript 和 Chrome 开发者工具。
2. 设置并探索游乐场
访问代码
此 Codelab 包含很多指向 Lit 游乐场的如下链接:
该游乐场是一个完全在浏览器中运行的代码沙盒。它可以编译并运行 TypeScript 和 JavaScript 文件,还可以自动解析节点模块的导入。例如:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://unpkg.com/lit?module';
您可以从这些检查点着手,在 Lit 游乐场中完成整个教程。如果您使用 VS Code,那么您可以使用这些检查点下载任何步骤的起始代码并用其检查您的代码。
探索 Lit 游乐场界面

Lit 游乐场界面的屏幕截图突出显示了您在此 Codelab 中将要使用的部分。
- 文件选择器。请注意加号按钮…
- 文件编辑器。
- 代码预览。
- “重新加载”按钮。
- 下载按钮。
VS Code 设置(高级)
使用此 VS Code 设置具有如下好处:
- 模板类型检查
- 模板智能感知和自动补全
如果您已安装 NPM 和 VS Code(带有 lit-plugin 插件),并了解如何使用该环境,那么您只需执行以下操作来下载和启动这些项目:
- 按下载按钮
- 将 tar 文件的内容提取到目录中
- 安装可解析裸模块说明符的开发服务器(Lit 团队建议使用 @web/dev-server)
- 以下是一个示例
package.json
- 以下是一个示例
- 运行开发服务器并打开浏览器(如果您使用的是
@web/dev-server,可以使用npx web-dev-server --node-resolve --watch --open)- 如果您使用的是示例
package.json,请使用npm run serve
- 如果您使用的是示例
3. 定义自定义元素
自定义元素
Web Components 是一组 4 个原生 Web API。它们分别是:
- ES 模块
- 自定义元素
- Shadow DOM
- HTML 模板
您已经使用过 ES 模块规范,该规范允许您创建具有导入和导出功能的 JavaScript 模块,这些模块通过 <script type="module"> 加载到网页中。
定义自定义元素
借助自定义元素规范,用户可以使用 JavaScript 定义自己的 HTML 元素。名称必须包含连字符 (-),以便与原生浏览器元素区分开来。清除 index.js 文件并定义自定义元素类:
index.js
class RatingElement extends HTMLElement {}
customElements.define('rating-element', RatingElement);
自定义元素通过将扩展 HTMLElement 的类与连字符分隔的标记名称相关联来定义。对 customElements.define 的调用会告知浏览器将类 RatingElement 与 tagName ‘rating-element' 相关联。这意味着,文档中名称为 <rating-element> 的每个元素都将与此类相关联。
在文档正文中放置一个 <rating-element>,看看呈现效果。
index.html
<body>
<rating-element></rating-element>
</body>
现在,查看输出内容,您会发现没有任何内容呈现。这是预期行为,因为您尚未告知浏览器如何呈现 <rating-element>。您可以在 Chrome 开发者版工具的元素选择器中选择 <rating-element>,然后在控制台中调用以下代码,确认自定义元素定义是否成功:
$0.constructor
该命令应输出:
class RatingElement extends HTMLElement {}
自定义元素生命周期
自定义元素附带一组生命周期钩子。它们分别是:
constructorconnectedCallbackdisconnectedCallbackattributeChangedCallbackadoptedCallback
当首次创建元素时(例如,通过调用 document.createElement(‘rating-element') 或 new RatingElement()),系统会调用 constructor。构造函数是设置元素的好地方,但出于元素“启动”性能方面的考虑,通常认为在构造函数中进行 DOM 操作是一种不好的做法。
当自定义元素附加到 DOM 时,系统会调用 connectedCallback。初始 DOM 操作通常在此阶段发生。
disconnectedCallback 在自定义元素从 DOM 中移除后调用。
当任何用户指定的属性发生变化时,系统都会调用 attributeChangedCallback(attrName, oldValue, newValue)。
当自定义元素通过 adoptNode(例如在 HTMLTemplateElement 中)从另一个 documentFragment 采用到主文档中时,系统会调用 adoptedCallback。
呈现 DOM
现在,返回到自定义元素并将其与一些 DOM 相关联。在元素附加到 DOM 时设置其内容:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
this.innerHTML = `
<style>
rating-element {
display: inline-flex;
align-items: center;
}
rating-element button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
在 constructor 中,您可以在元素上存储名为 rating 的实例属性。在 connectedCallback 中,您将 DOM 子元素添加到 <rating-element> 以显示当前评分,以及“我喜欢”和“不喜欢”按钮。
4. Shadow DOM
为何要使用 Shadow DOM?
在上一步中,您会注意到,您插入的样式标记中的选择器会选择网页上的任何评分元素以及任何按钮。这可能会导致样式从元素中泄露出来,并选择您可能不想设置样式的其他节点。此外,此自定义元素之外的其他样式可能会无意中设置自定义元素内节点的样式。例如,尝试将样式标记放在主文档的头部:
index.html
<!DOCTYPE html>
<html>
<head>
<script src="./index.js" type="module"></script>
<style>
span {
border: 1px solid red;
}
</style>
</head>
<body>
<rating-element></rating-element>
</body>
</html>
输出中应有一个红色边框,用于圈出评分对应的范围。这是一个微不足道的示例,但缺乏 DOM 封装可能会导致更复杂的应用出现更大的问题。这时,Shadow DOM 就派上用场了。
附加影子根
将 shadow 根附加到元素,并在该根内渲染 DOM:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
刷新页面后,您会发现主文档中的样式无法再选择 Shadow Root 内的节点。
您是怎么做到的?在 connectedCallback 中,您调用了 this.attachShadow,该方法会将影子根附加到元素。open 模式表示阴影内容可检查,并且还可通过 this.shadowRoot 访问阴影根。您也可以在 Chrome 检查器中查看 Web 组件:

您现在应该会看到一个可展开的影子根,其中包含相应内容。影子根内的所有内容都称为 Shadow DOM。如果您在 Chrome 开发者工具中选择评分元素并调用 $0.children,您会发现它不返回任何子元素。这是因为 Shadow DOM 不会被视为与直接子元素属于同一 DOM 树,而是属于 Shadow Tree。
Light DOM
实验:添加一个节点作为 <rating-element> 的直接子节点:
index.html
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
刷新页面,您会看到此自定义元素的 Light DOM 中的这个新 DOM 节点不会显示在页面上。这是因为 Shadow DOM 具有一些功能,可通过 <slot> 元素控制 Light DOM 节点如何投影到 Shadow DOM 中。
5. HTML 模板
为何要使用模板
使用 innerHTML 和未经过清理的模板字面量字符串可能会导致脚本注入安全问题。过去的方法包括使用 DocumentFragment,但这些方法也存在其他问题,例如在定义模板时加载图片和运行脚本,以及给可重用性带来障碍。这就是 <template> 元素发挥作用的地方;模板提供惰性 DOM、一种用于克隆节点的高性能方法,以及可重用的模板。
使用模板
接下来,将组件转换为使用 HTML 模板:
index.html
<body>
<template id="rating-element-template">
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating"></span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
</template>
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
</body>
在此示例中,您将 DOM 内容移到了主文档 DOM 中的模板标记内。现在,重构自定义元素定义:
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
const templateContent = document.getElementById('rating-element-template').content;
const clonedContent = templateContent.cloneNode(true);
shadowRoot.appendChild(clonedContent);
this.shadowRoot.querySelector('.rating').innerText = this.rating;
}
}
customElements.define('rating-element', RatingElement);
如需使用此模板元素,请查询模板、获取其内容,然后使用 templateContent.cloneNode 克隆这些节点,其中 true 实参会执行深度克隆。然后,使用数据初始化 DOM。
恭喜,您现在已拥有一个 Web 组件!不过,它目前还不会执行任何操作,因此接下来,请添加一些功能。
6. 添加功能
属性绑定
目前,设置评分元素评分的唯一方法是构造该元素,设置对象的 rating 属性,然后将其放置在网页上。遗憾的是,原生 HTML 元素通常不是以这种方式运作的。原生 HTML 元素往往会随着属性和特性变化而更新。
添加以下代码行,使自定义元素在 rating 属性发生更改时更新视图:
index.js
constructor() {
super();
this._rating = 0;
}
set rating(value) {
this._rating = value;
if (!this.shadowRoot) {
return;
}
const ratingEl = this.shadowRoot.querySelector('.rating');
if (ratingEl) {
ratingEl.innerText = this._rating;
}
}
get rating() {
return this._rating;
}
您为 rating 属性添加 setter 和 getter,然后更新评分元素的文本(如果可用)。这意味着,如果您在元素上设置评分属性,视图将会更新;您可以在开发者工具控制台中快速测试一下!
属性绑定
现在,当属性发生更改时,更新视图;这类似于在设置 <input value="newValue"> 时输入更新其视图。幸运的是,Web 组件生命周期包含 attributeChangedCallback。添加以下几行代码,以更新评分:
index.js
static get observedAttributes() {
return ['rating'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
}
}
为了触发 attributeChangedCallback,您必须为 RatingElement.observedAttributes which defines the attributes to be observed for changes 设置静态 getter。然后,您可以在 DOM 中以声明方式设置评分。不妨试试:
index.html
<rating-element rating="5"></rating-element>
现在,评分应该会以声明方式更新!
按钮功能
现在,还缺少按钮功能。此组件的行为应允许用户提供单个赞成或反对投票评分,并向用户提供视觉反馈。您可以使用一些事件监听器和一个反射属性来实现此目的,但首先请通过附加以下行来更新样式,以提供视觉反馈:
index.html
<style>
...
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
在 Shadow DOM 中,:host 选择器是指 Shadow Root 所附加到的节点或自定义元素。在这种情况下,如果 vote 属性为 "up",则赞按钮会变为绿色;但如果 vote 为 "down", then it will turn the thumb-down button red,则赞按钮会变为灰色。现在,通过为 vote 创建反射属性 / 特性来实现此逻辑,类似于您实现 rating 的方式。首先,我们从属性 setter 和 getter 开始:
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
}
get vote() {
return this._vote;
}
您可以在 constructor 中使用 null 初始化 _vote 实例属性,并在 setter 中检查新值是否不同。如果是,您需要相应地调整评分,并使用 this.setAttribute 将 vote 属性反映回宿主。
接下来,设置属性绑定:
index.js
static get observedAttributes() {
return ['rating', 'vote'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
} else if (attributeName === 'vote') {
this.vote = newValue;
}
}
同样,这与您之前使用 rating 属性绑定时所经历的过程相同;您将 vote 添加到 observedAttributes,并在 attributeChangedCallback 中设置 vote 属性。现在,最后一步是添加一些点击事件监听器,以赋予按钮功能!
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
this._boundOnUpClick = this._onUpClick.bind(this);
this._boundOnDownClick = this._onDownClick.bind(this);
}
connectedCallback() {
...
this.shadowRoot.querySelector('.thumb_up')
.addEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.addEventListener('click', this._boundOnDownClick);
}
disconnectedCallback() {
this.shadowRoot.querySelector('.thumb_up')
.removeEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.removeEventListener('click', this._boundOnDownClick);
}
_onUpClick() {
this.vote = 'up';
}
_onDownClick() {
this.vote = 'down';
}
在 constructor 中,您将一些点击监听器绑定到元素,并保留这些引用。在 connectedCallback 中,您监听按钮上的点击事件。在 disconnectedCallback 中,您需要清理这些监听器,并为点击监听器本身设置适当的 vote。
恭喜,您现在已拥有一个功能齐全的 Web 组件;不妨尝试点击一些按钮!现在的问题是,我的 JS 文件已达到 96 行,HTML 文件已达到 43 行,而且对于这样一个简单的组件,代码非常冗长且命令式。这时,Google 的 Lit 项目就派上用场了!
7. Lit-html
代码检查点
为什么选择 lit-html
首先,<template> 标记非常实用且性能出色,但它并未与组件的逻辑打包在一起,因此很难将模板与其余逻辑一起分发。此外,模板元素的固有使用方式也倾向于命令式代码,在许多情况下,与声明式编码模式相比,这会导致代码的可读性降低。
这时,lit-html 就派上用场了!Lit html 是 Lit 的渲染系统,可让您在 JavaScript 中编写 HTML 模板,然后高效地渲染和重新渲染这些模板以及数据,以创建和更新 DOM。它与热门的 JSX 和 VDOM 库类似,但在浏览器中以原生方式运行,在许多情况下效率更高。
使用 Lit HTML
接下来,迁移原生 Web 组件 rating-element 以使用 Lit 模板,该模板使用带标记的模板字面量,这些字面量是使用特殊语法将模板字符串作为实参的函数。然后,Lit 会在后台使用模板元素来提供快速渲染,并提供一些用于安全性的清理功能。首先,通过向 webcomponent 添加 render() 方法,将 index.html 中的 <template> 迁移到 Lit 模板:
index.js
// Dont forget to import from Lit!
import {render, html} from 'lit';
class RatingElement extends HTMLElement {
...
render() {
if (!this.shadowRoot) {
return;
}
const template = html`
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
<button class="thumb_down">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
render(template, this.shadowRoot);
}
}
您还可以从 index.html 中删除模板。在此渲染方法中,您定义了一个名为 template 的变量,并调用了 html 带标记的模板字面量函数。您还会注意到,您已使用 ${...} 的模板字面值插值语法在 span.rating 元素内执行了简单的数据绑定。这意味着,您最终将不再需要强制更新该节点。此外,您还可以调用 lit render 方法,该方法可将模板同步渲染到影子根中。
迁移到声明式语法
现在您已移除 <template> 元素,请重构代码以改为调用新定义的 render 方法。您可以先利用 Lit 的事件监听器绑定来清理监听器代码:
index.js
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
...
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
Lit 模板可以使用 @EVENT_NAME 绑定语法向节点添加事件监听器,在本例中,每次点击这些按钮时,您都会更新 vote 属性。
接下来,清理 constructor、connectedCallback 和 disconnectedCallback 中的事件监听器初始化代码:
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
connectedCallback() {
this.attachShadow({mode: 'open'});
this.render();
}
// remove disonnectedCallback and _onUpClick and _onDownClick
您能够从所有三个回调中移除点击监听器逻辑,甚至完全移除 disconnectedCallback!您还能够从 connectedCallback 中移除所有 DOM 初始化代码,从而使代码看起来更加简洁。这也意味着,您可以摆脱 _onUpClick 和 _onDownClick 监听器方法!
最后,更新属性 setter 以利用 render 方法,以便在属性或特性发生变化时更新 DOM:
index.js
set rating(value) {
this._rating = value;
this.render();
}
...
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
// add render method
this.render();
}
在此处,您能够从 rating setter 中移除 DOM 更新逻辑,并从 vote setter 中添加对 render 的调用。现在,您可以清楚地看到绑定和事件监听器的应用位置,因此模板的可读性大大提高。
刷新页面,您应该会看到一个可正常运行的评分按钮,当您按下赞成按钮时,该按钮应如下所示!

8. LitElement
为什么选择 LitElement
代码仍然存在一些问题。首先,如果您更改 vote 属性或特性,可能会更改 rating 属性,从而导致 render 被调用两次。尽管重复调用 render 本质上是空操作,并且非常高效,但 JavaScript 虚拟机仍然会花费时间同步调用该函数两次。其次,添加新属性和特性非常繁琐,需要大量样板代码。这时,LitElement 就派上用场了!
LitElement 是 Lit 的基类,用于创建快速、轻量的 Web 组件,这些组件可用于各种框架和环境。接下来,我们来看看 LitElement 在 rating-element 中能为我们做些什么,方法是更改实现以使用它!
使用 LitElement
首先,从 lit 软件包中导入 LitElement 基类并对其进行子类化:
index.js
import {LitElement, html, css} from 'lit';
class RatingElement extends LitElement {
// remove connectedCallback()
...
您导入了 LitElement,它是 rating-element 的新基类。接下来,保留 html import,最后是 css,这使我们能够为 CSS 数学、模板和其他幕后功能定义 CSS 带标记的模板字面量。
接下来,将样式从渲染方法移至 Lit 的静态样式表:
index.js
class RatingElement extends LitElement {
static get styles() {
return css`
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
`;
}
...
这是 Lit 中大多数样式所在的位置。Lit 会采用这些样式,并使用 Constructable Stylesheets 等浏览器功能来缩短渲染时间,并在必要时通过旧版浏览器上的 Web Components polyfill 传递这些样式。
Lifecycle
Lit 在原生 Web Component 回调的基础上引入了一组渲染生命周期回调方法。当声明的 Lit 属性发生更改时,系统会触发这些回调。
如需使用此功能,您必须静态声明哪些属性会触发渲染生命周期。
index.js
static get properties() {
return {
rating: {
type: Number,
},
vote: {
type: String,
reflect: true,
}
};
}
// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()
在此处,您定义了 rating 和 vote 将触发 LitElement 渲染生命周期,并定义了用于将字符串属性转换为属性的类型。
<user-profile .name=${this.user.name} .age=${this.user.age}>
${this.user.family.map(member => html`
<family-member
.name=${member.name}
.relation=${member.relation}>
</family-member>`)}
</user-profile>
此外,vote 属性上的 reflect 标志会自动更新您在 vote setter 中手动触发的主机元素的 vote 属性。
现在您已经有了静态属性块,可以移除所有属性和属性渲染更新逻辑。这意味着您可以移除以下方法:
connectedCallbackobservedAttributesattributeChangedCallbackrating(setter 和 getter)vote(setter 和 getter,但保留来自 setter 的更改逻辑)
您需要保留 constructor,并添加新的 willUpdate 生命周期方法:
index.js
constructor() {
super();
this.rating = 0;
this.vote = null;
}
willUpdate(changedProps) {
if (changedProps.has('vote')) {
const newValue = this.vote;
const oldValue = changedProps.get('vote');
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
}
}
// remove set vote() and get vote()
在此处,您只需初始化 rating 和 vote,并将 vote setter 逻辑移至 willUpdate 生命周期方法。每当任何更新属性发生更改时,都会在 render 之前调用 willUpdate 方法,因为 LitElement 会批量处理属性更改并异步进行渲染。对 willUpdate 中响应式属性(如 this.rating)的更改不会触发不必要的 render 生命周期调用。
最后,render 是 LitElement 生命周期方法,需要我们返回 Lit 模板:
index.js
render() {
return html`
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
}
您不再需要检查影子根,也不再需要调用之前从 'lit' 软件包导入的 render 函数。
您的元素现在应会在预览中呈现;点击该元素!
9. 恭喜
恭喜,您已成功从头开始构建了一个 Web 组件,并将其发展为 LitElement!
Lit 非常小(经缩减大小和 gzip 压缩后小于 5kb),速度非常快,而且编码体验非常有趣!您可以使用它构建可供其他框架使用的组件,也可以使用它构建功能完善的应用!
现在,您已经了解了什么是 Web 组件、如何构建 Web 组件,以及 Lit 如何让构建 Web 组件变得更加轻松!
代码检查点
您想对照我们的代码检查您的最终代码吗?点击此处进行比较。
后续操作
不妨看看其他 Codelab!
深入阅读
- Lit 互动教程
- Lit Docs
- Open Web Components - 一个由社区运营的指南和工具社区
- WebComponents.dev - 在所有已知框架中创建 Web 组件
社区
- Lit and Friends Slack - 最大的 Web 组件社区
- Twitter 上的@buildWithLit - Lit 团队的 Twitter 账号
- Web Components SF - 旧金山的 Web 组件聚会