从 Web 组件到 Lit 元素

1. 简介

上次更新日期:2021 年 8 月 10 日

Web 组件

Web 组件是一组 Web 平台 API,可用于创建可在网页和 Web 应用中使用的新自定义、可重用、已封装 HTML 标记。根据网络组件标准构建的自定义组件和小部件适用于现代浏览器,并能与任何支持 HTML 的 JavaScript 库或框架结合使用。

什么是 Lit

Lit 是一个简单的库,用于构建快速、轻量级 Web 组件,该组件可在任何框架中运行,也可能根本无需框架。借助 Lit,您可以构建可共享的组件、应用、设计系统等。

Lit 提供了 API 来简化常见的 Web 组件任务,例如管理属性、属性和渲染。

学习内容

  • 什么是网络组件
  • Web 组件的概念
  • 如何构建网络组件
  • 什么是 lit-html 和 LitElement
  • Lit 在 Web 组件之上执行的操作

构建内容

  • 香草“赞”/“不喜欢”Web 组件
  • 基于 Lit 的 Web 组件“我喜欢 / 不喜欢”

所需条件

  • 任何更新后的新型浏览器(Chrome、Safari、Firefox、Chromium Edge)。Web 组件可在所有新型浏览器中运行,并且 polyfill 适用于 Microsoft Internet Explorer 11 和非 Chromium Microsoft Edge。
  • 了解 HTML、CSS、JavaScript 和 Chrome 开发者工具

2. 准备工作探索游乐场

访问代码

在整个 Codelab 中,都会提供如下指向 Lit 游乐场的链接:

Playground 是一个完全在浏览器中运行的代码沙盒。它可以编译和运行 TypeScript 和 JavaScript 文件,还可以自动解析对节点模块的导入。例如

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://unpkg.com/lit?module';

您可以使用这些检查点作为起点,在 Lit 园地中完成整个教程。如果您使用的是 VS Code,则可以使用这些检查点下载任何步骤的起始代码,并使用这些代码检查您的工作。

探索 Lit 游乐场界面

文件选择器标签页栏标为第 1 节,代码修改部分标为第 2 节,输出预览标为第 3 节,预览重新加载按钮标为第 4 节

Lit Playground 界面的屏幕截图突出显示了您将在此 Codelab 中使用的部分。

  1. 文件选择器。请注意加号按钮...
  2. 文件编辑器。
  3. 代码预览。
  4. “重新加载”按钮。
  5. 下载按钮。

VS Code 设置(高级)

以下是使用此 VS Code 设置的好处:

  • 模板类型检查
  • 模板智能感知和自动补全

如果您已安装 NPM、VS Code(带有 lit-plugin 插件),并了解如何使用该环境,则只需执行以下操作即可下载并启动这些项目:

  • 按下载按钮
  • 将 tar 文件的内容解压缩到目录中
  • 安装可解析裸模块说明符的开发服务器(Lit 团队建议使用 @web/dev-server
  • 运行开发服务器并打开浏览器(如果您使用的是 @web/dev-server,则可以使用 npx web-dev-server --node-resolve --watch --open
    • 如果您使用的是示例 package.json,请使用 npm run serve

3. 定义自定义元素

自定义元素

Web 组件包含 4 个原生 Web API。它们是:

  • ES 模块
  • 自定义元素
  • 阴影 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 与标记名称 ‘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 {}

自定义元素生命周期

自定义元素附带一组生命周期钩子。它们是:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

首次创建元素时,系统会调用 constructor:例如,通过调用 document.createElement(‘rating-element')new RatingElement()。构造函数非常适合用来设置元素,但对于“启动”元素,在构造函数中进行 DOM 操作通常被视为一种不良做法。性能原因。

当自定义元素附加到 DOM 时,系统会调用 connectedCallback。这通常是进行初始 DOM 操作的地方。

从 DOM 中移除自定义元素后,系统会调用 disconnectedCallback

当用户指定的任何属性发生更改时,系统会调用 attributeChangedCallback(attrName, oldValue, newValue)

当通过 adoptNode 将自定义元素从另一个 documentFragment 合并到主文档时(如在 HTMLTemplateElement 中),系统会调用 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. 阴影 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 的用武之地。

附加影子根

将影子根附加到元素,并在该根内部渲染 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);

刷新页面时,您会注意到主文档中的样式无法再选择阴影根内的节点。

您是如何做到这一点的?在 connectedCallback 中,您调用了 this.attachShadow,它会将影子根附加到元素。open 模式表示阴影内容可检查,并使阴影根也可通过 this.shadowRoot 访问。再看看 Chrome 检查器中的网络组件:

Chrome 检查器中的 dom 树。有一个 <rating-element>其中 a#shadow-root (open) 作为其子项,而之前的 DOM 则位于该 shadowroot 中。

您现在应该看到一个可展开的影子根,用于存放内容。该影子根内的所有内容称为 Shadow DOM。如果您在 Chrome 开发者工具中选择评分元素并调用 $0.children,您会发现它未返回任何子元素。这是因为 Shadow DOM 不属于直接子 DOM 树,而是 Shadow Tree 的一部分。

轻量级 DOM

实验:添加节点作为 <rating-element> 的直接子级:

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

刷新页面,您会发现该自定义元素的 Light DOM 中的这个新 DOM 节点没有显示在网页上。这是因为 Shadow DOM 提供了一些功能,用于控制 Light DOM 节点如何通过 <slot> 元素投影到阴影域内。

5. HTML 模板

为什么要使用模板

使用未经清理的 innerHTML 和模板字面量字符串可能会导致脚本注入出现安全问题。过去的方法包括使用 DocumentFragment,但这些还存在其他问题,例如在定义模板时图片加载和运行脚本,以及给可重用性带来了障碍。这就是 <template> 元素的用武之地:模板提供了 inert 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。

恭喜,您现在拥有了一个网络组件!遗憾的是,它目前还无法执行任何操作,因此接下来添加一些功能。

6. 添加功能

属性绑定

目前,在 rating-element 上设置评分的唯一方法是构建相应元素,在该对象上设置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;
}

您可以为评分属性添加 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.setAttributevote 属性反映给主机。

接下来,设置属性绑定:

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

接下来,迁移原生网络组件rating-element以使用 Lit 模板,该模板使用带标记模板字面量,这些字面量是将模板字符串作为参数并采用特殊语法的函数。然后,Lit 在后台使用模板元素提供快速渲染以及一些清理功能,以提高安全性。首先,向 web 组件添加 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 元素内执行了简单的数据绑定。这意味着您最终不再需要强制更新该节点。此外,您还可以调用照明的 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 以及 connectedCallbackdisconnectedCallback 中的事件监听器初始化代码:

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 的调用。现在,模板的可读性大大提升,因为您现在可以看到绑定和事件监听器的应用位置。

刷新页面后,您应该会得到一个正常运行的评分按钮,当用户按下点赞按钮时,该按钮应如下所示!

“我喜欢”和“不喜欢”评分滑块,值为 6 且“我喜欢”和“不喜欢”评分滑块为绿色

8. LitElement

为什么选择 LitElement

代码仍然存在一些问题。首先,如果更改 vote 属性或属性,可能会更改 rating 属性,这会导致调用 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 导入,最后保留 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 会采用这些样式,并使用可构造样式表等浏览器功能缩短呈现时间,并根据需要通过旧版浏览器上的网络组件 polyfill 传递这些内容。

Lifecycle

在原生 Web 组件回调的基础上,Lit 引入了一组渲染生命周期回调方法。当声明的 Lit 属性发生更改时,就会触发这些回调。

如需使用此功能,您必须静态声明哪些属性将触发渲染生命周期。

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

在这里,您可以定义 ratingvote 将触发 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 属性。

现在,您已经有了静态属性块,接下来可以移除所有属性和属性渲染更新逻辑了。这意味着您可以移除以下方法:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating(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()

在这里,您只需初始化 ratingvote,并将 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 超小(缩减大小后小于 5kb 加 gzip 压缩),超快,而且编码非常有趣!您可以使组件供其他框架使用,也可以使用组件构建成熟的应用!

现在,您已经了解了什么是 Web 组件、如何构建 Web 组件,以及 Lit 如何让构建变得更加轻松!

代码检查点

您想对照我们的代码检查您的最终代码吗?不妨点击此处进行比较。

后续操作

查看一些其他 Codelab!

深入阅读

社区