Web Components 是一种 Web 技术,它允许你创建可复用的自定义元素(Custom Elements)和封装功能强大的组件。Web Components 由三个主要技术组成:
- Custom Elements(自定义元素):一组 JavaScript API,允许你定义 custom elements 和它们的行为。
- Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的 Shadow DOM 树附加到元素(与主文档 DOM 分开)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML Templates(HTML 模板):通过
<template>
和 <slot>
元素,使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
实现 web component 的基本方法如下:
- 创建一个类或函数来指定 web 组件的功能。
- 使用
CustomElementRegistry.define()
方法注册你的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
- 如果需要的话,使用
Element.attachShadow()
方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
- 如果需要的话,使用
<template>
和 <slot>
定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到你的 shadow DOM 中。
- 在页面任何你喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样。
下面是一个简单的示例:
把这段代码保存成一个 html
文件,浏览器打开就能看到效果。代码看不懂没关系,接下来我们将详细介绍如何创建并使用 Web Components。
Custom Elements(自定义元素)
我们上面提到了 Web Components 的三个主要技术,第一个就是能够创建自定义元素,这些元素扩展了浏览器中可用的元素集。
自定义元素有两种类型:
- Customized built-in elements:自定义内置元素,继承自内置 HTML 元素(比如
HTMLParagraphElement
)的自定义元素。它们继承了内置元素的行为,但可以添加额外的功能。
- Autonomous custom elements:独立自定义元素,继承自 HTMLElement。
创建自定义元素
对于这两种类型的自定义元素,创建方式是一样的,只是继承的类不同。
如果是自定义内置元素,需要继承自内置元素的类,比如继承自 HTMLParagraphElement
的自定义元素:
如果是独立自定义元素,需要继承自 HTMLElement
:
注册自定义元素
创建之后我们需要使用 CustomElementRegistry.define()
方法注册自定义元素,之后才能在页面中使用。
define
方法接受三个参数:
name
:自定义元素的名称,必须包含一个连字符(-
)。
constructor
:自定义元素的构造函数。
options
:仅对于自定义内置元素,这是一个包含单个属性 extends
的对象,该属性是一个字符串,命名了要扩展的内置元素。
下面分别是注册自定义内置元素和独立自定义元素的示例:
使用自定义元素
定义和注册自定义元素之后,我们就可以在页面中使用它了:
要使用自定义内置元素,只需在 HTML 中使用扩展的内置元素名称,将自定义名称作为 is
属性的值:
要使用独立自定义元素,只需在 HTML 中使用自定义元素名称,就像使用常规 HTML 元素一样:
生命周期回调
一旦你的自定义元素被注册,当页面中的代码以特定方式与你的自定义元素交互时,浏览器将调用你的类的某些方法。通过提供这些方法的实现,规范称之为生命周期回调,你可以运行代码来响应这些事件。
自定义元素生命周期回调包括:
connectedCallback()
:每当元素添加到文档中时调用。规范建议开发人员尽可能在此回调中实现自定义元素的设定,而不是在构造函数中实现。
disconnectedCallback()
:每当元素从文档中移除时调用。
adoptedCallback()
:每当元素被移动到新文档中时调用。
attributeChangedCallback()
:在属性更改、添加、移除或替换时调用。
下面是一个简单的示例:
响应属性变化
与内置元素一样,自定义元素可以使用 HTML 属性来配置元素的行为。为了有效地使用属性,元素必须能够响应属性值的变化。为此,自定义元素需要将以下成员添加到实现自定义元素的类中:
-
observedAttributes
:一个静态属性,它是一个字符串数组,包含你想要监听的属性名称。
-
attributeChangedCallback()
:当元素的一个被监视的属性发生变化时,浏览器将调用此方法。
attributeChangedCallback()
回调在列在元素的 observedAttributes
属性中的属性被添加、修改、移除或替换时调用。它接收三个参数:
name
:属性的名称。
oldValue
:属性的旧值。
newValue
:属性的新值。
下面是一个简单的示例:
完整示例
所以一个完整的自定义元素示例可能是这样的:
独立自定义元素
在 HTML 中使用这个自定义元素:
自定义内置元素
在 HTML 中使用这个自定义元素:
Shadow DOM
Shadow DOM 是 Web Components 的一个重要特性,它允许你将封装的 Shadow DOM 树附加到元素(与主文档 DOM 分开)并控制其关联的功能。通过这种方式,你可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
上面示例的构造函数中已经展示了如何创建 Shadow DOM:
attachShadow()
方法接受一个配置对象,其中 mode
属性可以是 open
或 closed
。open
意味着外部代码可以访问 Shadow DOM,而 closed
意味着外部代码无法访问 Shadow DOM。
Shadow DOM 并不是 Web Components 的专属功能,下面是一个简单的示例:
使用 JavaScript 操作 Shadow DOM
默认情况下,Shadow DOM 是封闭的,这意味着它是私有的,外部代码无法访问。比如我们有如下代码:
点击按钮后,只有页面中的 span
元素被转换为大写,而 Shadow DOM 中的 span
元素没有被转换。
要访问 Shadow DOM 中的元素,首先把 attachShadow()
方法的 mode
参数设置成 open
, 接下来可以使用 shadowRoot
属性访问 Shadow DOM:
这样就可以把 Shadow DOM 中的 span
元素转换为大写了。
CSS 封装
页面的 CSS 不会影响 Shadow DOM 内的节点:
在这种情况下,页面中的 span
元素是蓝色的,有黑色边框,而 Shadow DOM 中的 span
元素是默认样式。
要在 Shadow DOM 中使用 CSS,有两种方法:
- 编程式:通过构建一个
CSSStyleSheet
对象并将其附加到影子根。
- 声明式:通过在一个
<template>
元素的声明中添加一个 <style>
元素。
在这两种情况下,Shadow DOM 树中定义的样式局限在该树内,所以就像页面样式不会影响 Shadow DOM 中的元素一样,Shadow DOM 样式也不会影响页面中其它元素的样式。
要通过构建 CSSStyleSheet
来为 Shadow DOM 添加样式,我们可以:
- 创建一个
CSSStyleSheet
对象。
- 使用
CSSStyleSheet.replace
或者 CSSStyleSheet.replaceSync
方法添加样式规则。
- 使用
shadowRoot.adoptedStyleSheets
属性将样式表附加到 Shadow DOM。
下面是一个示例:
构建 CSSStyleSheet
对象的一个替代方法是将一个 <style>
元素包含在用于定义 web 组件的 <template>
元素中。
使用哪种方式取决于你的应用程序和个人喜好。
创建一个 CSSStyleSheet
并通过 adoptedStyleSheets
将其赋给影子根允许你创建单一样式表并将其与多个 DOM 树共享。例如,一个组件库可以创建单个样式表,然后将其与该库的所有自定义元素共享。浏览器将仅解析该样式表。此外,你可以对样式表进行动态更改,并将更改传播到使用表的所有组件。
而当你希望是声明式的、需要较少的样式并且不需要在不同组件之间共享样式的时候,附加 <style>
元素的方法则非常适合。
模板和插槽
接下来我们介绍 HTML template
和 slot
,它们是 Web Components 的另一个重要特性。
当需要在网页中多次重用相同的 HTML 结构时,可以使用 template
。模板是一个 HTML 元素,它包含了不会被呈现的 HTML 内容。模板通常用于存储可重复使用的 HTML 片段,这些片段可以通过 JavaScript 复制和插入到文档中。
让我们看一个简单的示例:
要将它显示在页面中,需要使用 JavaScript 获取对它的引用,然后将其内容插入到 DOM 中:
模板本身就很有用,但与 web component 一起使用效果更好。
这里要注意的关键点是,我们将模版内容的克隆添加到通过 Node.cloneNode()
方法创建的影子根上。
由于我们添加了模板的内容到影子 DOM,我们可以在模板的 <style>
元素中包含一些样式信息,然后将其封装在自定义元素中。
比如:
除了模板,我们还可以使用插槽(slot)来插入内容,让你的组件更灵活。插槽是一种占位符,允许你将内容插入到自定义元素中。
如果想添加一个插槽,可以在模板中添加一个 <slot>
元素:
如果没有插入内容,那么默认文本将被显示。如果要插入内容,在使用自定义元素时,在 <slot>
元素中添加内容:
总结
在 JavaScript 框架的地位和能力不断提高的时代,Web Components 一直在努力获得认可和采用。现在大部分前端开发者都使用 React、Vue 或 Angular,Web Components 看起来可能很复杂且笨重,尤其是缺少数据绑定和状态管理等功能。
但 Web Components 作为一种标准化的 Web 技术,近年来也得到了显著的发展和广泛的支持。许多前端框架和库已经开始支持或集成 Web Components,使得它们更容易使用。
虽然有一些问题需要解决,但 Web Components 的未来仍然是值得期待的。