编写可在任何前端框架中工作的组件
浏览器有一种内置的方式以 Web 组件 的形式编写可重用组件。 它们是构建可在任何前端框架中运行的交互式、可重用组件的绝佳选择。 话虽如此,编写高度交互且强大的 Web 组件并不简单。 它们需要大量样板文件,并且感觉比您在 React、Svelte 和 Vue 等框架中编写的组件要直观得多。
在本文中,我将向您提供一个编写为 Web 组件的交互式组件的示例,然后使用一个软化边缘并删除大量样板文件的库对其进行重构。
如果您不熟悉 Web 组件,也不必担心。 在下一节中,我将(非常简短且有限地)概述 Web 组件是什么以及它们是由什么组成的。 如果您对它们有一些基本经验,则可以跳过下一部分。
什么是 Web 组件?
在 Web 组件出现之前,浏览器没有编写可重用组件的标准方法。 许多库都解决了这个问题,但它们经常遇到性能、互操作性和 Web 标准问题等限制。
它们是由 3 种不同的浏览器功能组成的技术:
- 自定义元素
- 影子 DOM
- HTML 模板
我们将对这些技术进行快速速成课程,但这绝不是全面的分解。
自定义元素
使用自定义元素,您可以创作自己的自定义 HTML 元素,并可以在整个站点中重复使用这些元素。 它们可以像文本、图像或视觉装饰一样简单。 您可以进一步推动它们并构建交互式组件、复杂的小部件或整个 Web 应用程序。
您不仅限于在项目中使用它们,还可以发布它们并允许其他开发人员在他们的网站上使用它们。
以下是我的库 A2K 中的一些可重用组件。 您可以看到它们有各种形状和大小,并且具有一系列不同的功能。 在项目中使用它们与使用任何旧的 HTML 元素类似。
以下是在项目中使用进度元素的方法:
<!DOCTYPE html>
<html>
<头>
<title>快速入门</title>
<元字符集=“UTF-8”/>
</头>
<正文>
<!-- 在 HTML 中像常规内置元素一样使用 Web 组件。 -->
<a2k-progress 进度=“50”/>
<!-- a2k Web 组件使用标准 JavaScript 模块。 -->
<脚本类型=“模块”>
导入'https://cdn.jsdelivr.net/npm/@a2000/progress@0.0.5/lib/src/a2k-progress.js';
</脚本>
</正文>
</html>
导入第三方脚本后,您可以开始使用组件“a2k-progress”,就像任何其他 HTML 元素一样。
如果您正在构建自己的 Web 组件,那么自定义元素的复杂程度几乎没有限制。 我最近创建了一个 Web 组件,可以在浏览器中呈现 CodeSandbox 代码编辑器。 因为它是一个 Web 组件,所以您可以在任何您喜欢的框架中使用它! 如果您想了解更多信息,您可以在此处阅读更多信息。
影子 DOM
如果您有 CSS 的应用知识,您就会知道普通 CSS 的作用范围是全球性的。 在你的 global.css 中写这样的东西:
p {
颜色: 番茄色;
}
假设没有其他更具体的 CSS 选择器应用于“p”元素,将为所有“p”元素提供漂亮的橙色/红色。
它具有由视觉设计驱动的鲜明特征。 您可能想要使用此组件,但如果您的全局样式影响字体系列、颜色或字体大小等内容,则可能会导致组件的外观出现问题:
<头>
<风格>
身体 {
颜色: 蓝色;
字体大小:12px;
字体系列:系统用户界面;
}
</风格>
</头>
<正文>
<a2k-选择></a2k-选择>
</正文>
![相同的选择菜单,但其许多定义特征被全局 CSS 覆盖。](https://component-odyssey.com/images/articles/01-writing-components-that-work-in-any- 框架/a2k-select-menu-2.png)
这就是 Shadow DOM 的用武之地。 Shadow DOM 是一种封装机制,可以防止 DOM 的其余部分干扰您的 Web 组件,从而确保Web 应用程序的全局样式不会干扰您使用的任何组件。 这也意味着组件库开发人员可以放心地编写组件,确保它们在不同的 Web 应用程序中的外观和行为符合预期。
当谈到 Shadow DOM 时,还有更多的细微差别,以及我们在本文中不会涉及的其他功能。 如果您想了解有关 Web 组件的更多信息,我有一门完整的课程 (Component Odyssey),专门教您如何构建在任何框架中工作的可重用组件 。
HTML 模板
我们短暂浏览的 Web 组件功能中的最后一个功能是 HTML 模板。
该 HTML 元素与其他元素的不同之处在于,浏览器不会将其内容呈现到页面上。 如果您要编写以下 HTML,您将不会在页面上看到文本“我是标题”:
<正文>
<模板>
<h1>我是标题</h1>
</模板>
</正文>
模板的内容不是用于直接呈现内容,而是用于复制。 然后可以使用复制的模板将内容呈现到页面。 您可以将模板元素视为 3D 打印的模板。 该模板不是物理实体,但它用于创建现实生活中的克隆。
然后,您可以在 Web 组件中引用模板元素,克隆它,并将克隆呈现为组件的标记。
我不会再花时间讨论这些 Web 组件功能,但您可能已经注意到,要编写普通 Web 组件,您需要了解和理解许多新的浏览器功能。 您将在下一节中看到,构建 Web 组件的心智模型并不像其他组件框架那样简化。
一个基本的 Web 组件
现在我们已经简要介绍了支持 Web 组件的基本技术,下面介绍如何构建 hello world 组件:
const template = document.createElement('template');
template.innerHTML = `<p>Hello World</p>`;
类 HelloWorld 扩展 HTMLElement {
构造函数(){
极好的();
this.attachShadow({ 模式: '打开' });
this.shadowRoot.append(template.content.cloneNode(true));
}
}
customElements.define('hello-world', HelloWorld);
这是人们可以编写的最简单的组件,但已经发生了很多事情。 对于那些完全不熟悉 Web 组件的人来说,如果没有我上面提供的背景知识,他们将会留下很多问题和很多困惑。
对我来说,至少有两个关键原因导致 Web 组件难以编写,至少在 hello world 示例的上下文中是这样。
标记与组件逻辑解耦
在许多框架中,组件的标记通常被视为一等公民。 它通常是从组件函数返回的内容,或者可以直接访问组件的状态,或者具有内置实用程序来帮助操作标记(如循环、条件等)。
Web 组件的情况并非如此。 事实上,标记通常是在组件类之外定义的。 模板也没有内置方法来引用组件的当前状态。 随着组件复杂性的增加,这将成为一个麻烦的限制。
在前端领域,组件旨在帮助开发人员在多个页面中重用标记。 因此,标记和组件逻辑有着千丝万缕的联系,因此它们应该彼此并置。
编写 Web 组件需要了解其所有底层技术
正如我们在上面看到的,Web 组件由三种技术组成。 您还可以在 hello world 代码片段中看到,我们明确需要了解并理解这三种技术。
- 我们正在创建一个模板元素并设置其内部 HTML
- 我们正在创建一个 shadow root,并显式地将其模式设置为“open”。
- 我们正在克隆我们的模板并将其附加到我们的影子根
- 我们正在向文档注册一个新的自定义元素
这本质上并没有什么问题,因为 Web 组件应该是“较低级别”的浏览器 API,这使得它们成为在其上构建抽象的主要工具。 但对于来自 React 或 Svelte 背景的开发人员来说,必须了解这些新的浏览器功能,然后必须用它们编写组件,可能会感觉太麻烦。
更高级的 Web 组件
让我们看一下更高级的 Web 组件:计数器按钮。
单击该按钮,计数器就会递增。
这以下示例包含一些额外的 Web 组件概念,例如生命周期函数和可观察属性。 您不需要了解代码片段中发生的所有事情。 这个例子实际上只是用来说明最基本的交互界面(一个计数器按钮)需要多少样板:
const templateEl = document.createElement("template");
templateEl.innerHTML = `
<按钮>按我!</按钮>
<p>你按了我0次。</p>
`;
导出类 OdysseyButton 扩展 HTMLElement {
构造函数(){
极好的();
this.attachShadow({ 模式: "打开" });
this.shadowRoot.appendChild(templateEl.content.cloneNode(true));
this.button = this.shadowRoot.querySelector("button");
this.p = this.shadowRoot.querySelector("p");
this.setAttribute("计数", "0");
}
// 注意:Web 组件有生命周期方法,
// 如果我们在组件添加到 DOM 时设置事件侦听器,那么我们的工作就是清理
// 当它从 DOM 中移除时它们会被删除
连接回调() {
this.button.addEventListener("点击", this.handleClick);
}
断开连接回调() {
this.button.removeEventListener("click", this.handleClick);
}
// 与 React 这样的框架不同,Web 组件在 prop(或属性)出现时不会自动重新渲染
// 变化。 相反,我们需要明确定义我们想要观察哪些属性。
静态获取观察属性(){
return ["禁用", "计数"];
}
// 当上述属性之一发生变化时,这个生命周期方法就会运行,我们可以
// 对新属性的值做出相应的反应。
attributeChangedCallback(名称, _, newVal) {
如果(名称===“计数”){
this.p.innerHTML = `你按了我 ${newVal} 次。`;
}
if (name === "禁用") {
this.button.disabled = true;
}
}
// 在 HTML 中,属性值始终是字符串。 这意味着我们的工作是
// 转换类型。 您可以在下面看到我们正在转换字符串 -> 数字,然后再转换回字符串
处理点击 = () => {
const counter = Number(this.getAttribute("count"));
this.setAttribute("count", `${counter + 1}`);
};
作为 Web 组件作者,我们需要考虑很多事情:
- 设置shadow DOM
- 设置 HTML 模板
- 清理事件监听器
- 定义我们想要观察的属性
- 当属性改变时做出反应
- 处理属性的类型转换
还有很多其他事情需要考虑,但我在本文中没有提及。
这并不是说 Web 组件不好,你不应该编写它们,事实上,我认为通过使用它们进行构建,你可以学到很多关于浏览器平台的知识。 但是,我认为如果您的首要任务是以更加简化和符合人体工程学的方式编写可互操作的组件,那么有更好的方法来编写组件。
用更少的样板编写 Web 组件
正如我之前提到的,有很多工具可以帮助您更轻松地编写 Web 组件。 其中一个工具叫做 Lit,它是由 Google 的一个团队开发的。 Lit 是一个轻量级库,旨在通过消除我们上面已经看到的样板文件来简化 Web 组件的编写。
正如我们将看到的,Lit 在底层做了很多繁重的工作,以帮助将代码总行数减少近一半! 而且由于 Lit 是 Web 组件和其他本机浏览器功能的包装器,因此您所有有关 Web 组件的现有知识都是可转移的。
要开始了解 Lit 如何简化 Web 组件,请参阅前面的 hello world 示例,但已重构为使用 Lit 而不是普通 Web 组件:
从“lit”导入{LitElement,html};
导出类 HelloWorld 扩展 LitElement {
使成为() {
return html`<p>Hello World!</p>`;
}
}`
customElements.define('hello-world', HelloWorld);
Lit 组件的样板代码少了很多,而且 Lit 处理我之前提到的两个问题的方式略有不同。 让我们看看如何:
- 标记直接在组件类中定义。 虽然您可以在类之外定义模板,但通常的做法是从“render”函数返回模板。 这更符合其他 UI 框架中呈现的心理模型,其中 UI 是状态的函数。
- Lit 也不需要开发人员附加影子 DOM,或者创建模板和克隆模板元素。 虽然了解底层 Web 组件功能将有助于开发 Lit 组件,但入门时不需要这些功能,因此进入门槛要低得多。
那么现在到了最后的结局,当我们将计数器组件迁移到 Lit 后,它会是什么样子呢?
导入{LitElement, html } 来自“lit”;
导出类 OdysseyCounter 扩展 LitElement {
静态属性={
// 我们定义组件的属性及其类型。
// 当这些属性的值发生变化时,将触发组件重新渲染。
// 虽然它们不一样,但您可以将这些“属性”视为
// Lit 的“观察属性”替代方案
// 如果该值作为属性向下传递,Lit 会转换该值
// 到正确的类型
计数:{ 类型:数字 },
禁用:{类型:布尔值},
};
构造函数(){
极好的();
// 无需创建影子 DOM,克隆模板,
// 或存储对 DOM 节点的引用。
这个.count = 0;
}
onCount() {
this.count = this.count + 1;
}
使成为() {
// 不使用 attributeChangedCallback 生命周期,
// 渲染函数可以访问组件的所有属性,
// 这简化了操作模板的过程。
返回 html`
<按钮?disabled=${this.disabled} @click=${this.onCount}>
按我!
</按钮>
<p>你按了我 ${this.count} 次。</p>
`;
}
}`
我们正在编写的代码量几乎减少了一半! 当创建更复杂的用户界面时,这种差异变得更加明显。
为什么我要继续谈论 Lit?
我是 Web 组件的忠实拥护者,但我认识到,对于许多开发人员来说,进入门槛很高。 编写复杂的 Web 组件需要了解大量浏览器功能,并且围绕 Web 组件的教育并不像 React 或 Vue 等其他技术那么全面。
这就是为什么我认为使用像 Lit 这样的工具可以使编写高性能和可互操作的 Web 组件变得更加容易。 如果您希望组件在任何前端框架中工作,这非常有用。
如果您想了解更多信息,这是我在即将推出的课程 Component Odyssey 中教授的方法。 对于任何想要了解如何编写在任何框架中工作的组件的人来说,本课程非常适合。 为此,我首先介绍 Web 组件的绝对基础知识,然后再介绍 Lit 等工具,这些工具可以简化 Web 组件的编写过程,而不会使您的开发环境变得复杂。 最后,您将学习如何构建和发布可跨任何前端框架工作的组件库。
如果您想要 Component Odyssey 的早鸟折扣代码,请前往前往网站获取通知。