Web 组件:简介
在现代 Web 开发中,框架风靡一时。几乎所有现代框架都有组件的概念。组件背后的理念是将前端逻辑分解为可重复使用的较小块,这些块可在页面或项目之间共享。通常,这些组件无法在其他框架之间重复使用,并且需要构建过程将它们编译为可在浏览器中运行的 JavaScript。
如果我告诉您有一种方法可以使用原生 JavaScript 和广泛使用的浏览器 API 构建组件,并可在各个框架之间共享,您会怎么想?现在,Web 组件已将这一点变为现实。在这里,我们将快速了解不同类型的 Web 组件,以及我们可以利用它们实现的一些功能。
Web 组件的基础知识
Web 组件是使用自定义元素注册表定义的。这是大多数现代浏览器提供的 API。要创建 Web 组件,只需在代码中定义它,然后在自定义元素注册表中注册它。一旦使用正确的命名约定注册和定义它,该组件即可在页面中使用。
customElements.define("my-component", MyComponentClass);
Web 组件的类型
Web 组件可分为两种不同的类别。它们是**自主 Web 组件**和**自定义内置元素**。
**Autonomous Web Components** 是通用 HTMLElement 类的扩展。这些组件通常更灵活,因为您实际上是在构建自己的 HTML 元素,并能够从头开始自定义所有行为。这包括用于呈现组件的根元素。定义后,您可以像使用任何其他 HTML 元素一样使用 Autonomous Web Components。
Button text
**自定义内置元素** 扩展特定的 HTML 元素。例如,您可以扩展 HTMLButtonElement 类或 HTMLAnchorElement。这些旨在增强现有 HTML 元素的功能。要使用自定义内置元素,请在要增强的 HTML 元素上使用“is”属性,以告知它是 Web 组件的一个实例。
命名 Web 组件
定义 Web 组件时,必须遵循某些约定。
通常,你会将组件命名为类似于 HTML 元素的名称,并附加自己的前缀以保持简单(即)。基本规则要求元素名称以小写字母开头,并且必须包含连字符。这些准则在大多数情况下都适用,但如果您对所有规则感兴趣,我建议您查看 HTML 规范。
<1234-button/>
生命周期钩子
Web 组件具有特定的生命周期钩子,用于对组件经历的不同阶段做出反应。钩子如下:
class MyComponent extends HTMLElement { static observedAttributes = ["btntype"] connectedCallback() { // Handle when the component is attached to the DOM } disconnectedCallback() { // Handle when the component is removed from the DOM } adoptedCallback() { // Handle when the component is attached to a new DOM } attributeChangedCallback(name, oldValue, newValue) { // Trigged when the "btntype" attribute is changed since it is in the list of observedAttributes. // "name" will be the name of the attribute that changed. // "oldValue" is the value before the change. // "newValue" is the new value after the change. } }
这些生命周期钩子用于执行创建/销毁组件实例时所需的任何初始化或清理工作。attributeChangedCallback 特别有用,因为它允许对属性值更新做出反应。Web 组件有一个特殊的静态属性,称为“observedAttributes”,它意味着是一个将触发 attributeChangedCallback 的属性名称(字符串)数组。
无障碍设施
可访问性是当今任何 Web 开发中的一个重要考虑因素。对于 Web 组件,您可以像在常规 HTML 或框架中一样使用 ARIA 属性,但一般来说,您将继承所使用的 HTML 元素的内置角色和可访问性功能。
所有准则都适用于其他地方。例如,确保在构建组件时使用语义 HTML,添加可能需要的任何必要的键盘处理,并确保正确管理焦点和颜色对比度等内容。
影子 DOM
Shadow DOM 可能是 Web Components 中最令人困惑和争议的部分。Shadow DOM 本质上是 DOM 中一个独立作用域的部分,位于 Web Component 中
Shadow DOM 主要关注的是自治 Web 组件,因为自定义内置元素只是添加到现有 HTML 元素中。对于自治 Web 组件,表示元素的自定义标签(即) 被视为“主机”元素。主机元素内是“影子根”。影子根内是组件的标记渲染位置。
下面是一个示例,您将看到“my-button”元素作为主机,其中包含 Shadow DOM。

构建 Web 组件时,您可以将 Shadow DOM 设置为两种模式。这些模式分别是“打开”和“关闭”。可以使用 Light DOM 中 Shadow Root 之外的 JavaScript 访问打开的 Shadow DOM,而不能访问关闭的 Shadow DOM。
class MyComponent extends HTMLElement { constructor() { const shadow = this.attachShadow({ mode: "open" }); // open or closed. } }
您在 Shadow DOM 中定义的任何样式都位于 Shadow DOM 内,不会污染文档的其余部分。在“Light DOM”(文档的其余部分)中定义的任何样式都不会渗透到 Shadow DOM(CSS 变量是个例外,但我们不会在这里讨论这个问题)。现代浏览器确实提供了使用 CSS 的 parts 直接从 Light DOM 定位 Shadow DOM 的方法。您可以通过将 part 属性添加到标记中来将 parts 添加到组件的 Shadow DOM。然后可以使用 ::part 伪选择器在 CSS 中定位这些部分。这非常方便,但它本质上非常有限。您不能将子选择器链接到 ::part 选择器。您只能定位 Shadow DOM 中具有“part”属性的特定元素。
在使用 Shadow DOM 时,可访问性也是一个重要的考虑因素。如果您曾经使用过 ARIA 属性,那么您对“aria-scribeby”和“aria-labelledby”一定不陌生,它们通常被赋予一个 ID,该 ID 引用另一个元素,该元素包含屏幕阅读器内容的标签或描述。Shadow DOM 将 ID 的范围与样式分开,因此您无法从 Light DOM 引用 Shadow DOM 中的 ID,反之亦然。当您尝试提供需要动态提供的详细描述时,这可能会带来挑战,但存在一些解决方法,我们不会在本介绍中深入探讨。
模板和插槽
模板和插槽是可与 Shadow DOM 结合使用以增强 Web 组件的工具。模板用于在 Web 组件中创建可重复使用的代码片段,而插槽用于暴露可将 Light DOM 中的内容传递到的“漏洞”。
如果您需要在 Web 组件中反复呈现 HTML 片段,那么模板就非常方便。它们也可以在 Web 组件之外使用,但使用情况更为有限。它们是使用“template”标签实现的。
插槽用于将内容从 Light DOM 传递到 Web 组件,并使用“slot”标记实现。如果您有一个通用组件,可能需要传递动态内容,那么这很方便。一个很好的例子可能是通用卡片组件,您可以在其中暴露一个插槽以将标记传递到卡片主体中。插槽有一个“name”属性,您可以提供该属性来唯一地标识插槽。如果您需要将多个插槽放入 Web 组件中,这很方便。在传递内容时,您只需传递一个值为 slot="your-slot-name" 的属性,内容就会传递给具有匹配名称的插槽。
插槽和 Shadow DOM 具有值得注意的独特交互。插槽可以具有默认内容,这些内容在没有传入任何内容的情况下会呈现。传入插槽的内容位于 Light DOM 中,并被“浅复制”到 Shadow DOM 中。您可以在浏览器检查器中直观地看到这一点。插槽内容将在 Web 组件中呈现,但在 DOM 中,内容在技术上位于 Web 组件之外并提供指向插槽的链接。

话虽如此,这意味着所有插槽内容的样式和引用都与 Light DOM 中的任何其他内容一样。Light DOM 中的样式会影响插槽内容,而 Shadow DOM 样式则不会。有 API 可用于从 Web 组件内部与插槽内容进行交互。
Web 组件支持
现代浏览器对 Web 组件的支持相当好。主要的例外是 Safari,它不支持自定义内置元素。如果您需要支持 Internet Explorer 11 等较旧的浏览器,则必须对某些内容进行 polyfill。
基本示例
现在我们已经了解了所有基本概念的简单介绍,让我们看一些例子。
自主定制元素
以下是名为“my-button”的自主自定义元素的示例:
// A class defining a custom my-button element. class MyButton extends HTMLElement { #btnType = "primary" #btnText = "" #block = false #button = null static observedAttributes = ["btntext", "btntype", "block"] constructor() { super(); } // Called when an attribute changes. attributeChangedCallback(attr, oldVal, newVal) { if (attr === "btntext" && oldVal !== newVal) { this.#btnText = newVal; } if (attr === "btntype" && oldVal === newVal) { this.#btnType = newVal; } if (attr === "block") { if (newVal === null) { this.#block = false this.#button && this.#button.classList.remove("my-button--block"); } else { this.#block = true this.#button && this.#button.classList.add("my-button--block"); } } this.render(); } // When the component is attached to the DOM. connectedCallback() { this.attachShadow({ mode: "open" }); this.#btnType = this.getAttribute("btntype") || this.#btnType; // Keep primary default this.#btnText = this.getAttribute("btntext"); this.#block = this.hasAttribute("block"); this.render(); } // Getters and setters. // Allows getting/setting properties on the element using element.{property} // Similar to how you might grab the value off an input using input.value. set btntype(value) { this.#btnType = value; this.render(); } get btntype() { return this.#btnType; } set btntext(value) { this.#btnText = value; this.render(); } get btntext() { return this.#btnText; } // Method for generating a class name based on the btntype attribute. #getBtnTypeClass() { if (this.#btnType === "secondary") { return "my-button--secondary"; } return "my-button--primary"; } // Not a lifecycle hook! // Custom helper method for rendering the component. render() { if (this.shadowRoot) { const buttonClasses = 'my-button ' + this.#getBtnTypeClass() + (this.#block ? ' my-button--block' : '') this.shadowRoot.innerHTML = ` `; this.#button = this.shadowRoot.querySelector("button"); } } } customElements.define("my-button", MyButton); // Register the element with the registry.
在这里我们可以看到“my-button”本质上是 HTML 按钮元素的包装器。我们添加了一些生活质量属性,用于自定义按钮文本和自定义按钮的外观。具体来说,“btntype”属性控制应用哪些类来更改按钮的颜色,“block”布尔属性控制按钮的 CSS 显示值。
我们已经做了一些工作来添加与属性相关的“props”。这样,我们可以通过更改实际属性本身(element.setAttribute())或直接更改元素上的 prop(element.btntype =“somethingelse”)来更改属性的值。
最后,我们添加了自己的自定义方法“render”,用于实际渲染内容。如果没有这个,我们的 Web 组件将什么都不显示!我们必须制定逻辑来实际显示内容。您会注意到,此方法在 ConnectedCallback 中被调用,因此当元素附加到 DOM 时,我们会进行渲染。老实说,这是一种相当不靠谱的渲染方式,但对于入门示例来说,这种方法可以正常工作。
现在,让我们看看如何将这些元素付诸实践:
在这里,您可以看到我们像使用其他 HTML 元素一样使用 my-button。我们只是传递属性并为其分配值,如果我们正确处理这些值,我们甚至可以传递布尔属性,就像在常规 HTML 中一样!
我们还有一个通过插槽传递内容的示例!另外,请注意如何使用 ::part 选择器来设置其中一个按钮的样式。
最后,请注意,我们可以像任何其他 HTML 元素一样将事件监听器附加到这些组件。
自定义内置元素
这是作为自定义内置元素完成的 my-button 的不同版本:
// A class defining a custom my-button element. // Here we explicitly extend HTMLButtonElement class MyButton extends HTMLButtonElement { #btnType = "primary" #btnText = "" #block = false static observedAttributes = ["btntext", "btntype", "block"] constructor() { super(); } // Called when an attribute changes. attributeChangedCallback(attr, oldVal, newVal) { if (attr === "btntext" && oldVal !== newVal) { this.#btnText = newVal; } if (attr === "btntype" && oldVal === newVal) { this.#btnType = newVal; } if (attr === "block") { if (newVal === null) { this.#block = false this.classList.remove("my-button--block"); } else { this.#block = true this.classList.add("my-button--block"); } } this.render(); } // When the component is attached to the DOM. connectedCallback() { this.#btnType = this.getAttribute("btntype") || this.#btnType; // Keep primary default this.#btnText = this.getAttribute("btntext"); this.#block = this.hasAttribute("block"); this.render(); } // Getters and setters. // Allows getting/setting properties on the element using element.{property} // Similar to how you might grab the value off an input using input.value. set btntype(value) { this.#btnType = value; this.render(); } get btntype() { return this.#btnType; } set btntext(value) { this.#btnText = value; this.render(); } get btntext() { return this.#btnText; } // Method for generating a class name based on the btntype attribute. #getBtnTypeClass() { if (this.#btnType === "secondary") { return "my-button--secondary"; } return "my-button--primary"; } // Not a lifecycle hook! // Custom helper method for rendering the component. render() { const buttonClasses = ['my-button', this.#getBtnTypeClass(), (this.#block ? 'my-button--block' : '')].filter(clazz => !!clazz); this.textContent = this.#btnText; this.classList.add(...buttonClasses); } } // Defined slightly differently // Notice we explicitly extend button here. customElements.define("my-button", MyButton, { extends: "button" }); // Register the element with the registry.
首先要注意的是,代码大部分是相同的。最大的区别是我们直接扩展了 HTMLButtonElement,然后在定义自定义元素时也声明了我们扩展了按钮。
我们还花费了更少的时间来编写用于呈现元素的代码。由于我们正在扩展 HTMLButtonElement,因此该组件只是一个具有额外功能的 HTML 按钮。我们将使用 HTML“is”属性告诉 HTML 按钮它是一个“my-button”。
以下是实际示例:
您会再次注意到,我们使用“is”属性来增强现有的 HTML 按钮元素。您还会注意到,就像使用 Autonomous 自定义元素一样,我们可以附加事件侦听器并像处理任何其他 HTML 元素一样处理按钮,这在这里更有意义,因为它实际上只是一个扩展的 HTML 按钮。
总结
Web 组件是解决创建可共享组件问题的一种简单方法,这些组件可以在不同的页面和项目中重复使用。它们的工作方式更像普通的 HTML 元素,这可能会造成一些混淆,但最终它们非常有用,并有助于解决现代框架所针对的许多相同问题。
这里我们简单介绍了 Web 组件、围绕它们的不同概念以及一些展示其基本功能的简单示例。从这里我们可以开始更深入地了解如何使构建和使用它们变得更容易,并研究如何处理它们的一些痛点。
如果您有兴趣,请随意查看 GitHub 中的示例,或者您可以在 Code Pen 中使用它们。