Web 组件:从零到精通(第二部分)
Web 组件:从零到精通(第二部分)
Web 组件:从零到精通(第二部分)
使用 lit-html 增强 Web 组件
如果你已经阅读过本博客系列的第一部分,那么你现在应该已经了解了 Web 组件的基础知识。如果你还没有阅读过,我建议你先阅读第一部分,因为我们将回顾并在此基础上拓展第一部分中介绍的许多概念。
在这篇博文中,我们将使用名为lit-html的渲染库来增强我们的待办事项应用程序。但在深入探讨之前,我们需要讨论一些事项。如果您仔细阅读,就会注意到我之前将我们的 Web 组件称为“原始Web 组件”。之所以这样称呼,是因为 Web 组件是底层组件,其设计本身就不包含模板或其他功能。Web 组件的初衷是作为一系列标准的集合,用于实现平台当时尚不支持的特定功能。
我想引用贾斯汀·法尼亚尼的话,所有 Web 组件的作用就是为开发者提供“何时”和“何地”的信息。“何时”指的是元素的创建、实例化、连接、断开连接等等。“何地”指的是元素实例和 Shadowroot。至于如何利用这些信息,则取决于你。
此外,lit-html并非框架,而是一个利用标准 JavaScript 语言特性的 JavaScript 库。库和框架之间的区别通常是一个颇具争议的话题,但我希望用Dave Cheney的这个比喻来定义它们:
框架基于好莱坞模式;不要给我们打电话,我们会给你打电话。
Lit-html 也非常轻巧,大小不到 2kb,而且渲染速度很快。
现在我们已经解决了这个问题,让我们来看看lit-html是如何工作的。
🔥 Lit-html
- [x] 了解 Lit-html
- [ ] Lit-html 的实践
- [ ] 增强我们的 Web 组件
- [ ] 属性、特性和事件
- [ ] 总结
Lit-html 是一个渲染库,它允许你使用 JavaScript 模板字符串编写 HTML 模板,并高效地将这些模板渲染到 DOM 中。带标签的模板字符串是 ES6 的一项特性,它可以跨越多行,并包含 JavaScript 表达式。一个带标签的模板字符串可能如下所示:
const planet = "world";
html`hello ${planet}!`;
带标签的模板字面量只是标准的 ES6 语法。而且这些标签实际上就是函数!请看下面的例子:
function customFunction(strings) {
console.log(strings); // ["Hello universe!"]
}
customFunction`Hello universe!`;
它们还可以处理表达式:
const planet = "world";
function customFunction(strings, ...values) {
console.log(strings); // ["Hello ", "! five times two equals "]
console.log(values); // ["world", 10]
}
customFunction`Hello ${planet}! five times two equals ${ 5 * 2 }`;
如果我们查看源代码,就会发现 lit-html 的工作原理也正是如此:
/**
* Interprets a template literal as an HTML template that can efficiently
* render to and update a container.
*/
export const html = (strings: TemplateStringsArray, ...values: any[]) =>
new TemplateResult(strings, values, 'html', defaultTemplateProcessor);
如果我们这样写:
const planet = "world";
function customFunction(strings, ...values) {
console.log(strings); // ["<h1>some static content</h1><p>hello ", "</p><span>more static content</span>"]
console.log(values); // ["world"]
}
customFunction`
<h1>some static content</h1>
<p>hello ${planet}</p>
<span>more static content</span>
`;
你会注意到,当我们把 `<div>`strings和`<div>` 的值输出values到控制台时,我们已经将模板的静态内容和动态部分分开了。这在我们想要跟踪更改并使用相应数据更新模板时非常有用,因为它允许我们只监控动态部分的变化。这与 VDOM 的工作方式也有很大不同,因为我们已经知道`<div>`<h1>和 `<div>`<span>是静态的,所以我们不需要对它们进行任何操作。我们只对动态部分感兴趣,它可以是任何 JavaScript 表达式。
lit-html 会读取你的模板,将所有表达式替换为名为Part`<div>` 的通用占位符,并<template>根据替换结果创建一个元素。这样我们就得到了一个 HTML 模板,它知道应该将接收到的数据放置在哪里。
<template>
<h1>some static content</h1>
<p>hello {{}}</p> <-- here's our placeholder, or `Part`
<span>more static content</span>
</template>
Lit 会记住这些占位符的位置,从而实现轻松高效的更新。Lit 还会高效地重复利用这些占位<template>符:
const sayHello = (name) => html`
<h1>Hello ${name}</h1>
`;
sayHi('world');
sayHi('universe');
为了提高效率,这两个模板将使用完全相同的代码<template>,唯一的区别在于我们传递的数据。如果您仔细阅读,就会记得我们在本系列博客的第一部分中使用了相同的技术。
Part我们模板中的动态元素可以是任何JavaScript 表达式。Lit-html 甚至不需要任何特殊处理来计算这些表达式,JavaScript 会自动完成这项工作。以下是一些示例:
简单的:
customFunction`<p>${1 + 1}</p>`; // 2
条件语句:
customFunction`<p>${truthy ? 'yes' : 'no'}</p>`; // 'yes'
我们甚至可以处理数组和嵌套:
customFunction`<ul>${arr.map(item => customFunction`<li>${item}</li>`)}</ul>`;
🚀 Lit-html 实践
- [x] 了解 Lit-html
- [x] Lit-html 的实践
- [ ] 增强我们的 Web 组件
- [ ] 属性、特性和事件
- [ ] 总结
那么让我们看看这在实践中是如何运作的:
import { html, render } from 'lit-html';
class DemoElement extends HTMLElement {
constructor() {
super();
this._counter = 0;
this._title = "Hello universe!";
this.root = this.attachShadow({ mode: "open"});
setInterval(() => {this.counter++}, 1000);
}
get counter() {
return this._counter;
}
set counter(val) {
this._counter = val;
render(this.template(), this.root);
}
template() {
return html`
<p>Some static DOM</p>
<h1>${this.counter}</h1>
<h2>${this._title}</h2>
<p>more static content</p>
`;
}
}
window.customElements.define('demo-element', DemoElement);
如果你读过本系列的第一篇博文,这部分内容应该会让你感到熟悉。我们创建了一个简单的 Web 组件,它每秒递增一个计数器,并且我们使用了 lit-html 来处理渲染工作。
精彩部分在这里:
return html`
<p>Some static DOM</p>
<h1>${this.counter}</h1>
<h2>${this._title}</h2>
<p>more static content</p>
`;
以及 DOM 中的输出:
现在我们可以看到,lit只更新代码中已更改的部分(this.counter),甚至不会处理静态部分。而且这一切都无需任何框架技巧或虚拟 DOM,库大小也只有不到 2kb!您可能还会注意到输出中有很多 HTML 注释;别担心,这是 lit-html 用来跟踪静态部分和动态部分位置的方式。
⚡️ 为我们的组件注入超强能量
- [x] 了解 Lit-html
- [x] Lit-html 的实践
- [x] 增强我们的 Web 组件
- [ ] 属性、特性和事件
- [ ] 总结
现在我们已经了解了 lit-html 的渲染原理,接下来让我们实践一下。你可以在这里和GitHub上查看完整的代码。我们将一步一步地讲解,但首先让我们概览一下这个功能强大的组件:
to-do-app.js:
import { html, render } from 'lit-html';
import './to-do-item.js';
class TodoApp extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ 'mode': 'open' });
this.todos = [
{ text: 'Learn about Lit-html', checked: true },
{ text: 'Lit-html in practice', checked: false },
{ text: 'Supercharge our web component', checked: false },
{ text: 'Attributes, properties, and events', checked: false },
{ text: 'Wrapping up', checked: false }
];
render(this.template(), this._shadowRoot, {eventContext: this});
this.$input = this._shadowRoot.querySelector('input');
}
_removeTodo(e) {
this.todos = this.todos.filter((todo,index) => {
return index !== e.detail;
});
}
_toggleTodo(e) {
this.todos = this.todos.map((todo, index) => {
return index === e.detail ? {...todo, checked: !todo.checked} : todo;
});
}
_addTodo(e) {
e.preventDefault();
if(this.$input.value.length > 0) {
this.todos = [...this.todos, { text: this.$input.value, checked: false }];
this.$input.value = '';
}
}
template() {
return html`
<style>
:host {
display: block;
font-family: sans-serif;
text-align: center;
}
button {
border: none;
cursor: pointer;
background-color: Transparent;
}
ul {
list-style: none;
padding: 0;
}
</style>
<h3>Raw web components + lit-html</h3>
<br>
<h1>To do</h1>
<form id="todo-input">
<input type="text" placeholder="Add a new to do"></input>
<button @click=${this._addTodo}>✅</button>
</form>
<ul id="todos">
${this.todos.map((todo, index) => html`
<to-do-item
?checked=${todo.checked}
.index=${index}
text=${todo.text}
@onRemove=${this._removeTodo}
@onToggle=${this._toggleTodo}>
</to-do-item>
`
)}
</ul>
`;
}
set todos(value) {
this._todos = value;
render(this.template(), this._shadowRoot, {eventContext: this});
}
get todos() {
return this._todos;
}
}
window.customElements.define('to-do-app', TodoApp);
了解了大致情况吗?太好了!你会发现我们的代码有很多地方都发生了变化,所以让我们仔细看看。
你可能首先注意到的是,我们处理组件渲染的方式已经彻底改变了。在之前的应用中,我们需要手动创建一个template元素,设置它的 innerHTML,克隆它,然后将其添加到 shadowroot 中。当我们需要更新组件时,我们必须创建一大堆元素,设置它们的属性,添加事件监听器,然后将它们添加到 DOM 中。所有这些都是手动完成的。光是想想就头疼。而现在,我们把所有的渲染工作都委托给了 lit-html。
现在我们只需声明一次模板,就可以在模板中以声明方式render设置属性、特性和事件,并在需要时调用 lit-html 的函数。lit-html 的优点在于其渲染速度快、效率高;它只关注动态表达式,并且只修改需要更新的内容。而且所有这一切都无需框架的额外开销!
你还会注意到,我们将 `$($ ( _addTodo$ _removeTodo($( _toggleTodo$($($($($($($($($($($($($($(($(( (( ( ( ( ...settodos
🔨 属性、特性和事件
- [x] 了解 Lit-html
- [x] Lit-html 的实践
- [x] 增强我们的 Web 组件
- [x] 属性、特性和事件
- [ ] 总结
接下来,我们来看看 lit-html 是如何处理属性、特性和事件的。
${this.todos.map((todo, index) => {
return html`
<to-do-item
?checked=${todo.checked}
.index=${index}
text=${todo.text}
@onRemove=${this._removeTodo}
@onToggle=${this._toggleTodo}>
</to-do-item>
`;
})}
你可能在组件的更新版本中看到过这种奇怪的语法,并且想知道它的含义。Lit-html 允许我们在模板中以声明式的方式设置属性、特性和事件处理程序,而不是以命令式的方式设置。由于我们在本系列的第一部分中已经学习了属性、特性和事件的相关知识,所以这部分内容应该很容易理解。如果你需要复习一下,我这里也有相关的资料。
让我们一步一步地来了解这一切。
💅 属性
text=${todo.text}
我们在 lit-html 中设置属性……就像在标准 HTML 中设置属性一样。唯一的区别在于,我们使用的是模板字符串中的动态值。我知道,这有点令人失望。以前我们必须像这样以命令式的方式设置属性:el.setAttribute('text', todo.text);。
✨嘿!听着!
常规属性仍然仅限于字符串类型!
☑️ 布尔属性
?checked=${todo.checked}
正如你从上一篇博文中了解到的,布尔属性的处理方式通常略有不同……
✨回忆✨
...
这意味着只有以下示例才是可接受的真值:
<div hidden></div> <div hidden=""></div> <div hidden="hidden"></div>还有一个是假的:
<div></div>
方便的是,lit-html 允许我们通过在属性名称前加上前缀来轻松地将属性指定为布尔?属性,然后确保该属性要么存在于元素上,要么不存在。
之前我们将布尔属性设置为:
if(todo.checked){
el.setAttribute('checked', '');
}
当我们的条件为假时,就完全省略了它。
📂 房产
.index=${index}
如果我们想要传递一些丰富的数据,例如数组或对象,或者像本例中那样传递一个数值,我们可以简单地使用点前缀。
以前,要设置组件的属性,我们必须使用命令式查询来获取组件并设置属性。现在,借助 lit-html,我们可以直接在模板中处理所有这些操作。
之前我们设置的属性如下:
el.index = index;
🎉 活动
@onRemove=${this._removeTodo}
最后,我们可以通过给事件监听器添加前缀来声明式地指定它们@。每当to-do-item组件触发onRemove事件时,this._removeTodo都会调用该监听器。非常简单。
再举一个例子,以下是处理点击事件的方法:
<button @click=${this._handleClick}></button>
✨嘿!听着!
注意我们
eventContext在render()函数中是如何指定 `<T>` 的:render(this.template(), this._shadowRoot, {eventContext: this});。这确保了我们this在事件处理程序中始终拥有对 `<T>` 的正确引用,并且使我们不必.bind(this)在 `<T>` 中手动编写事件处理程序constructor。
💭 总结
- [x] 了解 Lit-html
- [x] Lit-html 的实践
- [x] 增强我们的 Web 组件
- [x] 属性、特性和事件
- [x] 总结
如果你一路学到这里,你就已经走在成为真正的 Web Components 高手的路上了。你已经了解了 lit-html,lit-html 的渲染方式,如何使用属性、特性和事件,以及如何实现 lit-html 来处理 Web 组件的渲染。
干得漂亮!我们大幅提升了网页组件的性能,现在待办事项的渲染效率很高,但仍然有很多样板代码需要编写,而且属性和特性管理也比较繁琐。如果能有更简便的方法来处理这些就好了……
...什么?
……是鸟吗?🐦
……是飞机吗?✈️
它是...
💥 LitElement 💥
我们将在本博客系列的下一篇,也是最后一篇中详细讨论这个问题。感谢阅读!
文章来源:https://dev.to/thepassle/web-components-from-zero-to-hero-part-two-38p4

