关注【索引目录】服务号,更多精彩内容等你来探索!
Web 组件的真正问题很简单:没有清晰定义的成功使用方法。没有单一的指南。直到最近,该标准一直在变化。在某些方面,现在仍然如此。不过,核心 API 已经稳定下来,许多开发者正在积极投身其中。
然而,对于那些从未关注过该标准演变的人来说,可能很难找到入门的途径。学习使用这套强大工具所需的概念是一项挑战。关于这个主题的文章很多,但真正有用的信息却散落在过时的教程中,这些教程演示了多年来一直在变化的实验性功能。
因此,如果您想知道如何从头开始制作自己的组件,那么您正在阅读正确的文章。
Web 组件的剖析
让我们从基础知识开始。Web 组件如此实用的关键在于其能够创建自定义 HTML 元素。您可能也听说过 HTML 模板和 Shadow DOM。这些附加 API 使我们能够控制浏览器的原生封装方法,但目前还不需要理解它们。在深入探讨这些高级主题之前,让我们先来了解一下浏览器如何处理自定义元素。
理解自定义元素
解析 HTML 文档时,会根据找到的标签创建元素实例。浏览器熟悉诸如 、 或 之类的标准元素。但是,当您尝试创建自己的标签时会发生<div>什么<p>?<button>
<component></component>
浏览器无法对每个无法识别的标签都大发雷霆。它们也无法直接丢弃原本应该传递给用户的数据。因此,它们会将这些未描述的标签初始化为 的实例,HTMLUnknownElement并将其内容渲染为纯文本,甚至继续解析嵌套 HTML 的子树。这种行为正是我们能够定义自定义元素的基础。
定义自定义元素
HTML 拥有一套定义明确的标签。这套标签很少更改。HTML5 发布时,我们新增了一些语义元素,但之后就没再新增多少了。过去,我们只能期盼新元素的出现。之后,我们又对<div>其他元素进行了复杂的抽象。现在,我们可以将这些抽象精简为可复用的组件,为文档增添意义。但 HTML 根命名空间需要保留,以便将来内置标签使用。因此,如果您要创建自己的标签,则必须至少包含一个连字符-。
<my-element>Hello World</my-element>
<my-next-element>
<span> This </span>
<span> is </span>
<span> a </span>
<span> test </span>
</my-next-element>
通过这样做,我们告诉浏览器这不再是一个未知元素。我们将对此负责。该元素将被解析为通用内联元素HTMLElement,与 类似<span>。
您可以使用 CSS 来定位它们,就像任何其他元素一样。
my-element {
display: block;
padding: 1em;
color: black;
background: green;
}
my-next-element {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
}
您可以使用 JS 查询它们,就像任何其他元素一样。
const myElement = document.querySelector('my-element');
console.log(myElement instanceof HTMLElement); // true
const myNextElement = document.querySelector('my-next-element');
console.log(myNextElement.children); // HTMLCollection(4)
所有基本HTMLElement界面功能均可用,例如事件。
myElement.addEventListener('click', event => {
console.log(event.target);
})
但我们才刚刚触及 Web 组件世界的表面。
注册自定义元素
为了将通用元素转换为有意义的元素,我们需要在浏览器中注册它。
class MyElement extends HTMLElement {
constructor() {
super();
}
}
customElements.define('my-element', MyElement);
上面的示例本身并没有提供任何好处,除了我们已经通过标记实现的功能。但它为我们新的模块化、可复用组件奠定了基础。在自定义元素出现之前,我们必须:等待 DOM 完全解析,查询它以选择某个特定元素的所有实例,然后使用外部脚本将我们的功能包装在这些元素上。
有了自定义元素,我们现在可以将该逻辑直接嵌入到元素的定义中。我们不再需要在 DOM 中搜索元素。每次遇到我们的元素时,它都会自动使用我们为其定义的类。
利用力量
注册元素让我们能够灵活地进行各种操作。我们可以解析元素中的子元素,检测新的子元素,添加我们自己的子元素。我们可以随心所欲地操作元素。这完全取决于你的用例。每个组件都各不相同,可能性无穷无尽。
但我们必须从某个地方开始。所以,让我们构建一个简单的示例。我们将创建一个<copy-text>元素。当点击此元素时,它的文本内容将被复制到剪贴板。
我们可以通过一些 JavaScript 代码来实现这一点<span>,但使用一个专用元素可以让我们的标记意图更加清晰。它还允许我们在样式表中创建独特的规则,而无需过多的类或 ID 范围限制。而且,我们无需更改任何代码即可添加更多元素实例。
注意我在类定义中使用了#private方法。这使我们能够保护组件的内部功能,使其无法从外部进行操作。稍后讲解封装时,这一点会变得更加重要。目前,我们创建了一个完全位于根文档中的组件。页面的样式表完全覆盖了该元素。其整个结构可以通过文档上下文访问。
重用自定义元素
自定义元素在每个浏览上下文中只需初始化一次。将组件定义作为内联脚本或获取脚本添加进去,就足以让我们在任何地方随意使用它。
<!DOCTYPE html>
<title> Copy Text Element Example </title>
<link rel="stylesheet" href="theme.css">
<script type="module" src="copy-text.js"></script>
<div>
<label> Example: </label>
<copy-text title="Copy Your API Key">
A_REALLY_LONG_API_KEY_OR_WHATEVER
</copy-text>
</div>
<div>
<label> Example 2: </label>
<copy-text>
Some more text to copy
</copy-text>
</div>
如果我们从模块中导出该类,我们也可以将其导入到其他脚本中并创建自定义实例。
// at end of copy-text.js
export { CopyTextElement }
// in another component
import { CopyTextElement } from './copy-text.js';
const copyText = new CopyTextElement();
但是,如果您 100% 确信模块已加载,则无需执行此操作。在这种情况下,我们可以像其他元素一样简单地创建一个实例。如果需要,我们也可以从元素注册表中获取类。
const copyText = document.createElement('copy-text');
const CopyTextElement = customElements.get('copy-text');
const anotherCopy = new CopyTextElement();
如果您无法控制组件的加载顺序,您也可以等待元素被注册。
customElements.whenDefined('copy-text')
.then(CopyTextElement => {
const copyText = new CopyTextElement();
});
我们必须更深入地研究
到目前为止,我们仅仅讨论了自定义元素本身的行为方式。现在我们需要处理包含子树的元素。在传统的 Web 应用中,我们会通过定位宿主元素来选择元素集合。使用自定义元素,我们可以让元素本身负责其子元素。处理这种情况的最佳方法是使用Mutation Observer。
我们设置初始事件处理程序,然后动态地从进入或离开子树的任何元素中添加或删除它们。注意,我们在构造函数中初始化所有内容。虽然许多教程都将其作为connectedCallback生命周期中最重要的部分,但事实并非如此。构造函数是设置组件核心功能的最佳位置。
这样做的主要原因是为了让我们的元素能够立即处理子元素,而不必等到它连接到 DOM 后再处理。这样一来,我们就可以通过在添加到文档之前就已配置好的代码来创建此元素的实例。在本例中,我们只添加了点击处理程序。但如果有必要,我们可以将其用于更复杂的逻辑。这样一来,我们的元素就无需在插入后延迟渲染,而是可以立即使用。
const clickHander = document.createElement('click-handler');
[ 'test', 'another', 'more' ].forEach(label => {
const span = document.createElement('span');
span.textContent = label;
clickHandler.append(span);
});
requestAnimationFrame(() => {
document.append(clickHandler);
});
优雅灵活
我们已经了解了自定义元素如何以简单、模块化的方式实现可复用功能。无需构建工具或框架依赖,只需使用有意义的标记、经典 CSS 和原生 JS。Web 组件可在任何地方运行。但到目前为止,我们所做的只是简化了现有的方法。这个工具包还能提供更多功能。
在下一篇文章中,我们将探讨如何利用 HTML 模板轻松地将简单的声明式定义升级为复杂的标记结构。我们还将介绍如何充分利用 Shadow DOM 来完全或选择性地封装我们的组件。
在此之前,不妨尝试构建你自己的自定义元素。先从简单的开始,专注于解决实际问题。以下是一些激发创意的点子:
<format-number>:显示具有特定格式的数字(货币、小数等)<tool-tip>:悬停或点击时显示一些弹出描述性文字。<marquee-text>:回归最早的非标准元素之一。<nav-bar>:自动突出显示活动路线的网站菜单。关注【索引目录】服务号,更多精彩内容等你来探索!

