大家好,这里是大家的林语冰。
免责声明
本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 WRITING COMPONENTS THAT WORK IN ANY FRONTEND FRAMEWORK。
浏览器有一种以Web Component(Web 组件)的形式编写可复用组件的内置方法。它们是构建可在任何前端框架中运行的交互式可复用组件的不二法门。话虽如此,编写高度交互且鲁棒的 Web Component 并不简单。
它们需要大量模板文件,并且感觉比您在 React、Svelte 和 Vue 等框架中编写的组件要直观得多。
在本文中,我将表演一个交互式组件编写为 Web Component 的示例,然后使用一个软化边缘并删除大量模板文件的库对其进行重构。
如果您不熟悉 Web Component,也不必担心。在下一节中,我将(非常简短且有限地)概述 Web Component 是什么以及它们是由什么组成的。如果您对它们有一些基本经验,则可以跳过下一节。
Web Component 是什么?
在 Web Component 出现之前,浏览器没有编写可复用组件的标准方法。许多库都解决了此问题,但它们经常遇到性能、互操作性和 Web 标准问题等限制。
它们是由 3 种不同的浏览器功能组成的技术:
- 自定义元素
- Shadow DOM(影子 DOM)
- HTML 模板
我们将对这些技术"走码观猫",但这绝不是抽丝剥茧。
自定义元素
使用自定义元素,您可以创作自己的自定义 HTML 元素,并可以在整个站点中重复使用这些元素。它们可以像文本、图像或视觉装饰一样简单。您可以更进一步并构建交互式组件、复杂部件或整个 Web App。
您不仅限于在项目中使用它们,还可以发布它们并允许其他开发者在它们的网站上使用。
以下是我的 A2K 库中的若干可复用组件。您可以看到它们有各种形状和尺寸,并且具有一大坨不同功能。在项目中使用它们与使用任何旧的 HTML 元素类似。
以下是在项目中使用进度条元素的方法:
html
<!doctype html>
<html>
<head>
<title>Quick Start</title>
<meta charset="UTF-8" />
</head>
<body>
<!-- 像普通的内置元素一样在 HTML 中使用 Web Component。 -->
<a2k-progress progress="50" />
<!-- a2k web component 使用 JS 模块。 -->
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@a2000/progress@0.0.5/lib/src/a2k-progress.js'
</script>
</body>
</html>
导入第三方脚本后,您就可以开始像这样使用 a2k-progress
组件,就像其他 HTML 元素一样。
如果您正在构建自己的 Web Component,那么自定义元素的复杂程度几乎没有限制。我最近创建了一个 Web Component,可以在浏览器中呈现 CodeSandbox 代码编辑器。
因为它是一个 Web Component,所以您可以在任何您喜欢的框架中使用它!
Shadow DOM
如果您有 CSS 的应用知识,您就会知道普通 CSS 的作用域是全局的。在你的 global.css
中这样写,如下所示:
css
p {
color: tomato;
}
假设没有其他更具体的 CSS 选择器应用于 p
元素,这会给所有 p
元素提供漂亮的橙/红色。
以此选择菜单为例:
它具有由视觉设计驱动的鲜明特征。您可能想要使用此组件,但如果您的全局样式影响字体系列、颜色或字体大小等内容,则可能会导致组件的外观出现问题:
html
<head>
<style>
body {
color: blue;
font-size: 12px;
font-family: system-ui;
}
</style>
</head>
<body>
<a2k-select></a2k-select>
</body>
这就是 Shadow DOM 的用武之地。Shadow DOM 是一种封装机制,可以防止 DOM 的其余部分干扰您的 Web Component,这可以确保 Web App 的全局样式不会干扰您使用的任何组件。
这也意味着,组件库开发者可以放心编写组件,确保它们在不同的 Web App 中的外观和行为符合预期。
HTML 模板
我们"走码观猫"的最后一个 Web Component 的功能是 HTML 模板。
该 HTML 元素与其他元素的差异在于,浏览器不会将其内容渲染到页面上。如果您要编写下述的 HTML,您将不会在页面上看到文本"I'm a header":
html
<body>
<template>
<h1>I'm a header</h1>
</template>
</body>
模板的内容不是用于直接渲染内容,而是用于复制。然后可以使用复制的模板将内容渲染到页面。您可以将模板元素视为 3D 打印的模板。
该模板不是物理实体,但它用于创建现实生活中的克隆。
然后,您可以在 Web Component 中引用模板元素,克隆它,并将克隆渲染为组件标记。
您将在下一节中看到,构建 Web Component 的心智模型并不像其他组件框架那样简单粗暴。
基本的 Web Component
现在我们已经概述了支持 Web Component 的基本技术,下面介绍如何构建 hello world 组件:
js
const template = document.createElement('template')
template.innerHTML = `<p>Hello World</p>`
class HelloWorld extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.shadowRoot.append(template.content.cloneNode(true))
}
}
customElements.define('hello-world', HelloWorld)
这是我们可以编写的最简单的组件,但麻雀虽小,五脏俱全。
于我而言,至少有两个关键原因导致 Web Component 难以编写,至少在 hello world 示例的上下文中是这样。
解耦标记与组件逻辑
在许多框架中,组件标记通常被视为一等公民。
它通常是从组件函数返回的内容,或者可以直接读写组件状态,或者具有内置工具来辅助操作标记(比如循环、条件等)。
Web Component 的情况并非如此。事实上,标记通常定义在组件类之外。模板也没有内置方法来引用组件的当前状态。
随着组件复杂性熵增,这将成为一个头大的限制。
在前端领域,组件旨在辅助开发者在多个页面中重用标记。因此,标记和组件逻辑有着千丝万缕的联系,它们应该相濡以沫。
编写 Web Component 需要了解其所有底层技术
如上所示,Web Component 由三种技术组成。您还可以在 hello world 代码片段中看到,我们明确需要了解并理解这三种技术。
- 我们创建了一个模板元素 并设置其
innerHTML
- 我们创建了一个shadow root ,并显式地将其模式设置为
open
。 - 我们克隆了模板 并将其附加到shadow root中
- 我们在文档中注册了一个新的自定义元素
这本质上并没有什么问题,因为 Web Component 应该是"较较低阶"的浏览器 API,这使得它们成为在其上构建抽象的主要工具。
但对于 React 或 Svelte 背景的开发者而言,不得不了解这些新的浏览器功能,然后必须用它们编写组件,可能会有点头大。
高级 Web Component
让我们瞄一眼更高级的 Web Component:计数器按钮。
单击该按钮,计数器就会递增。
以下示例包含若干额外的 Web Component 概念,比如生命周期函数和可观察属性。您不需要了解代码片段中发生的所有事情。
这个例子实际上只是用来说明最基本的交互界面(一个计数器按钮)需要多少样板:
js
const templateEl = document.createElement("template");
templateEl.innerHTML = `
<button>Press me!</button>
<p>You pressed me 0 times.</p>
`;
export class OdysseyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(templateEl.content.cloneNode(true));
this.button = this.shadowRoot.querySelector("button");
this.p = this.shadowRoot.querySelector("p");
this.setAttribute("count", "0");
}
// 注意: Web components 有生命周期方法,
// 如果我们在将组件添加到 DOM 时设置事件侦听器,
// 当它从 DOM 移除时,清理它们是我们的工作
connectedCallback() {
this.button.addEventListener("click", this.handleClick);
}
disconnectedCallback() {
this.button.removeEventListener("click", this.handleClick);
}
// 不同于 React 等框架,当 prop 或 attribute 变化时,Web Component 不会自动渲染。
// 相反,我们需要显式定义需要观测的 attribute。
static get observedAttributes() {
return ["disabled", "count"];
}
// 当上述属性之一变化时,此生命周期方法会运行,
// 并且我们对新属性的值作出反应。
attributeChangedCallback(name, _, newVal) {
if (name === "count") {
this.p.innerHTML = `You pressed me ${newVal} times.`;
}
if (name === "disabled") {
this.button.disabled = true;
}
}
// 在 HTML 中,attribute 值总是字符串。
// 这意味着,我们需要转换类型。
// 如下所示,我们正在转换 string-> number,然后再转换回 string
handleClick = () => {
const counter = Number(this.getAttribute("count"));
this.setAttribute("count", `${counter + 1}`);
};
作为 Web 组件作者,我们需要考虑一大坨事情:
- 设置 shadow DOM
- 设置 HTML 模板
- 清理事件监听器
- 定义想要观察的属性
- 当 prop 变化时做出响应
- 处理 attribute 的类型转换
这并不是说 Web Component 不好或您不应该编写它们,事实上,我认为通过使用它们进行构建,您可以习得一大坨浏览器平台的知识。
但是,我认为如果您的首要任务是以更加简化和符合人体工程学的方式编写可互操作的组件,那么有更好的方法来编写组件。
用更少的样板编写 Web Component
如上所述,有一大坨工具可以辅助您更轻松地编写 Web Component。其中一个工具叫做 Lit,它是由一个谷歌团队开发的。Lit 是一个轻量级库,旨在通过移除上述的样板文件来简化编写 Web Component。
正如我们将看到的,Lit 在底层做了一大坨繁重工作,以此将代码总行数减少近一半!而且由于 Lit 是 Web Component 和其他原生浏览器功能的包装器,因此您所有关于 Web Component 的现有知识都可转移。
要开始了解 Lit 如何简化 Web Component,下面是之前的 hello world示例,但已使用 Lit 重构而不是普通 Web Component:
js
import { LitElement, html } from "lit";
export class HelloWorld extends LitElement {
render() {
return html`<p>Hello World!</p>`;
}
}`
customElements.define('hello-world', HelloWorld);
Lit 组件的样板代码少了很多,而且 Lit 处理我之前提到的两个问题的方式略有不同:
- 标记直接定义在组件类中。虽然您可以在类外部定义模板,但通常的做法是从
render
函数返回模板。这更符合其他 UI 框架中呈现的心理模型,其中 UI 是状态的函数。 - Lit 也不要求开发者附加 shadow DOM,或创建模板和克隆模板元素。虽然了解底层 Web Component 功能有助于开发 Lit 组件,但入门时不需要了解,因此入门门槛要低得多。
最后一步,当我们将计数器组件迁移到 Lit 会是什么样子呢?
js
import { LitElement, html } from "lit";
export class OdysseyCounter extends LitElement {
static properties = {
// 我们定义组件的属性以及它们的类型。
// 当 prop 的值变化时,这会触发组件重新渲染。
// 虽然它们不一样,但你可以想象这些"properties"是 Lit 对"可观察 attributes"的替代方案
// 如果该值作为 attribute 传递,Lit 将其转换为正确类型
count: { type: Number },
disabled: { type: Boolean },
};
constructor() {
super();
// 无需创建 shadow DOM,克隆模板,或存储 DOM 节点引用。
this.count = 0;
}
onCount() {
this.count = this.count + 1;
}
render() {
// 作为使用 attributeChangedCallback 的替换方案,
// render 函数可以读写组件的所有属性,
// 这简化了模板操作的过程。
return html`
<button ?disabled=${this.disabled} @click=${this.onCount}>
Press me!
</button>
<p>You pressed me ${this.count} times.</p>
`;
}
}`
我们编写的代码量几乎减少了一半!当创建更复杂的 UI 时,这种差异更加明显。
我为什么要继续谈论 Lit?
我是 Web Component 的迷弟,但我认识到,对于许多开发者而言,入门门槛很高。
编写复杂的 Web Component 需要了解一大坨浏览器功能,并且围绕 Web Component 的教程并不像 React 或 Vue 等其他技术那么全面。
这就是为什么我认为使用像 Lit 这样的工具可以简化编写高性能和可互操作的 Web Component。如果您希望组件在任何前端框架中工作,这非常有用。
友情赞助
您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~