Web Components 的力量

A video player with a corner folded over, revealing the code that powers it.

背景

自从第一个动画 DHTML 光标轨迹和“本周网站”徽章出现在网页上以来,可重用代码一直是网页开发人员的诱惑。从那段辉煌的日子开始,将第三方 UI 集成到您的网站就一直是,嗯,一个半脆弱的头痛问题。

使用其他人的聪明代码需要大量的样板 JavaScript 或涉及可怕的 !important 的 CSS 冲突。在 React 和其他现代框架的世界里,情况有所改善,但仅为重用小部件而需要完整框架的开销有点过分。HTML5 引入了一些新元素,例如 <video><input type="date">,这些元素为 Web 平台添加了一些急需的通用 UI 小部件。但是,为每个足够常见的 Web UI 模式添加新的标准元素并不是一个可持续的选择。

作为回应,制定了一系列 Web 标准。每个标准都有一些独立的效用,但当它们一起使用时,它们能够实现以前无法使用原生实现的、并且难以伪造的功能:创建可以像传统 HTML 一样出现在所有相同位置的用户定义 HTML 元素的功能。这些元素甚至可以从使用它们的站点中隐藏其内部复杂性,就像丰富的表单控件或视频播放器一样。

标准的演变

作为一个整体,这些标准被称为 Web Components。在 2018 年,很容易将 Web Components 视为旧闻。事实上,早期版本的标准自 2014 年起就以某种形式存在于 Chrome 中,而 polyfills 一直笨拙地填补着其他浏览器的空白。

经过在标准委员会的长时间打磨,Web Components 标准从其早期版本(称为版本 0)改进到了更加成熟的版本 1,该版本正在所有主流浏览器中实现。Firefox 63 添加了对两个支柱标准的支持,即自定义元素和 Shadow DOM,因此我认为现在是时候更仔细地研究一下如何扮演 HTML 发明家的角色了!

鉴于 Web Components 已经存在一段时间了,还有许多其他资源可用。本文旨在作为入门指南,介绍一系列新功能和资源。如果您想深入了解(您绝对应该这样做),您最好在 MDN Web 文档Google Developers 网站上阅读更多关于 Web Components 的内容。

定义您自己的工作 HTML 元素需要浏览器以前没有赋予开发人员的新功能。我将在每个部分中指出这些以前不可能实现的功能,以及它们借鉴了哪些其他较新的 Web 技术。

<template> 元素:回顾

第一个元素并不像其他元素那么新,因为它解决的需求先于 Web Components 的努力。有时您只需要存储一些 HTML。也许是您需要多次复制的一些标记,也许是您现在还不需要创建的一些 UI。<template> 元素获取 HTML 并解析它,而不会将解析后的 DOM 添加到当前文档中。

<template>
  <h1>This won't display!</h1>
  <script>alert("this won't alert!");</script>
</template>

如果解析后的 HTML 不添加到文档中,它会去哪里?它被添加到一个“文档片段”中,最好理解为包含 HTML 文档一部分的薄包装器。文档片段在附加到其他 DOM 时会消失,因此它们非常适合保存您以后需要的一组元素,这些元素位于您不需要保留的容器中。

“好吧,现在我在一个消失的容器中有一些 DOM,我如何在需要时使用它?”

您可以简单地将模板的文档片段插入到当前文档中

let template = document.querySelector('template');
document.body.appendChild(template.content);

这工作得很好,除了您刚刚销毁了文档片段!如果您两次运行上面的代码,您将收到错误,因为第二次 template.content 不存在了。相反,我们希望在插入之前复制片段

document.body.appendChild(template.content.cloneNode(true));

cloneNode 方法的功能与它听起来一样,它接受一个参数,指定是仅复制节点本身还是包含所有子节点。

template 标签非常适合您需要重复 HTML 结构的任何情况。当定义组件的内部结构时,它特别方便,因此 <template> 被纳入了 Web Components 俱乐部。

新功能

  • 一个保存 HTML 但不会将其添加到当前文档中的元素。

回顾主题

自定义元素

自定义元素是 Web Components 标准的典型代表。它如其名——允许开发人员定义自己的自定义 HTML 元素。使其成为可能并使之易于使用很大程度上依赖于 ES6 的类语法,而 v0 语法要复杂得多。如果您熟悉 JavaScript 中的类 或其他语言,您可以定义从其他类继承或“扩展”的类

class MyClass extends BaseClass {
// class definition goes here
}

那么,如果我们尝试一下呢?

class MyElement extends HTMLElement {}

直到最近,这都是一个错误。浏览器不允许扩展内置的 HTMLElement 类或其子类。自定义元素解除了此限制。

浏览器知道 <p> 标签映射到 HTMLParagraphElement 类,但它如何知道将哪个标签映射到自定义元素类?除了扩展内置类之外,现在还有一个“自定义元素注册表”来声明此映射

customElements.define('my-element', MyElement);

现在页面上的每个 <my-element> 都与 MyElement 的新实例相关联。每当浏览器解析 <my-element> 标签时,都会运行 MyElement 的构造函数。

标签名称中的破折号是怎么回事?嗯,标准机构希望在将来创建新的 HTML 标签,这意味着开发人员不能随便创建 <h7><vr> 标签。为了避免将来的冲突,所有自定义元素都必须包含破折号,并且标准机构承诺永远不会创建包含破折号的新 HTML 标签。冲突避免!

除了在创建自定义元素时调用构造函数之外,还有许多其他“生命周期”方法在自定义元素的不同时刻被调用

  • 当元素被附加到文档时,会调用 connectedCallback。这可能会发生不止一次,例如,如果元素被移动或删除然后重新添加。
  • disconnectedCallbackconnectedCallback 的对应方法。
  • 当元素上的白名单中的属性被修改时,attributeChangeCallback 会触发。

一个更丰富的示例如下所示

class GreetingElement extends HTMLElement {
  constructor() {
    super();
    this._name = 'Stranger';
  }
  connectedCallback() {
    this.addEventListener('click', e => alert(`Hello, ${this._name}!`));
  }
  attributeChangedCallback(attrName, oldValue, newValue) {
    if (attrName === 'name') {
      if (newValue) {
        this._name = newValue;
      } else {
        this._name = 'Stranger';
      }
    }
  }
}
GreetingElement.observedAttributes = ['name'];
customElements.define('hey-there', GreetingElement);

在页面上使用它将如下所示

<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>

但是,如果您想扩展现有的 HTML 元素呢?您绝对可以这样做,也应该这样做,但是,在标记中使用它们看起来大不相同。假设我们希望我们的问候语是一个按钮

class GreetingElement extends HTMLButtonElement

我们还需要告诉注册表我们正在扩展现有标签

customElements.define('hey-there', GreetingElement, { extends: 'button' });

因为我们正在扩展现有标签,所以实际上我们使用的是现有标签,而不是自定义标签名称。我们使用新的特殊 is 属性来告诉浏览器我们使用的是哪种按钮

<button is="hey-there" name="World">Howdy</button>

乍一看可能有点笨拙,但辅助技术和其他脚本在没有此特殊标记的情况下无法知道我们的自定义元素是一种按钮。

从这里开始,所有经典的 Web 小部件技术都适用。我们可以设置许多事件处理程序,添加自定义样式,甚至使用 <template> 冲出内部结构。人们可以通过 HTML 模板、DOM 调用甚至新的框架(其中许多框架在它们的虚拟 DOM 实现中支持自定义标签名称)将您的自定义元素与他们自己的代码一起使用。因为界面是标准的 DOM 界面,所以自定义元素允许真正可移植的小部件。

新功能

  • 扩展内置的 'HTMLElement' 类及其子类的能力
  • 一个自定义元素注册表,可通过 customElements.define() 使用
  • 检测元素创建、插入 DOM、属性更改等的特殊生命周期回调。

回顾主题

Shadow DOM

我们已经创建了友好的自定义元素,甚至还添加了一些时髦的样式。 我们希望在所有网站上使用它,并将代码与其他人共享,以便他们可以在自己的网站上使用它。 当我们定制的 `<button>` 元素直接遇到其他网站的 CSS 时,我们如何防止冲突的噩梦? Shadow DOM 提供了一种解决方案。

Shadow DOM 标准引入了“影子根”的概念。 表面上,影子根具有标准的 DOM 方法,并且可以像任何其他 DOM 节点一样附加。 影子根的优势在于其内容不会显示在包含其父节点的文档中。

// attachShadow creates a shadow root.
let shadow = div.attachShadow({ mode: 'open' });
let inner = document.createElement('b');
inner.appendChild(document.createTextNode('Hiding in the shadows'));
// shadow root supports the normal appendChild method.
shadow.appendChild(inner);
div.querySelector('b'); // empty

在上面的示例中,`<div>` “包含” `<b>`,并且 `<b>` 被渲染到页面,但传统的 DOM 方法无法看到它。 不仅如此,包含页面的样式也无法看到它。 这意味着影子根外部的样式无法进入,而影子根内部的样式也不会泄漏出去。 此边界 **不** 旨在作为安全功能,因为页面上的另一个脚本可以检测到影子根的创建,并且如果你对影子根有引用,你可以直接查询其内容。

影子根的内容通过向根添加 `<style>`(或 `<link>`)来设置样式

let style = document.createElement('style');
style.innerText = 'b { font-weight: bolder; color: red; }';
shadowRoot.appendChild(style);
let inner = document.createElement('b');
inner.innerHTML = "I'm bolder in the shadows";
shadowRoot.appendChild(inner);

哇,我们现在真的可以使用 `<template>`! 无论哪种方式,`<b>` 将受到根中样式表的影响,但任何匹配 `<b>` 标签的外部样式都不会影响。

如果自定义元素具有非影子内容怎么办? 我们可以使用一个名为 `<slot>` 的新特殊元素来使它们和谐共处

<template>
  Hello, <slot></slot>!
</template>

如果该模板附加到影子根,则以下标记

<hey-there>World</hey-there>

将呈现为

Hello, World!

这种将影子根与非影子内容组合起来的能力使你能够创建具有复杂内部结构的丰富自定义元素,这些结构在外部环境中看起来很简单。 插槽比我这里展示的功能更强大,具有多个插槽、命名插槽和特殊的 CSS 伪类来定位插槽内容。 你需要阅读更多内容!

新功能

  • 一种称为“影子根”的半隐藏 DOM 结构
  • 用于创建和访问影子根的 DOM API
  • 影子根内的作用域样式
  • 用于处理影子根和作用域样式的新 CSS 伪类
  • `<slot>` 元素

将它们组合在一起

让我们创建一个花哨的按钮! 我们将发挥创意,将该元素命名为 `<fancy-button>`。 什么使它变得花哨? 它将具有自定义样式,并且还允许我们提供图标并使其看起来很时髦。 我们希望按钮的样式无论在哪个网站上使用都保持花哨,因此我们将把样式封装在影子根中。

你可以在下面的交互式示例中看到完成的自定义元素。 务必查看自定义元素的 JS 定义和元素样式和结构的 HTML `<template>`。

结论

构成 Web Components 的标准基于这样的理念:通过提供多个低级功能,人们将以在编写规范时没有人预料到的方式将它们组合在一起。 自定义元素已被用于简化 在网络上构建 VR 内容,催生了 多个 UI 工具包,以及更多。 尽管标准化过程很长,但 Web Components 的新兴承诺将更多权力交给了创作者手中。 现在该技术已在浏览器中可用,Web Components 的未来掌握在你手中。 你会构建什么?


5 条评论

  1. Tornike

    第一个代码片段有 SyntaxError :D
    bug: alert(‘this won’t alert!’);
    fix: alert(‘this won\’t alert!’);

    2018 年 11 月 15 日 下午 1:16

    1. Potch

      捕捉得好! 将修复引号情况。

      2018 年 11 月 15 日 下午 2:37

  2. Feross Aboukhadijeh

    很棒的文章! 我一直在玩 Web Components,并发布了一个名为“bg-sound”的 npm 包,它模拟了 HTML 元素。 它允许你使用简单的 Web Components 在浏览器中播放 MIDI 文件。 https://github.com/feross/bg-sound

    Web Components 比我想象的更容易使用。 非常棒!

    2018 年 11 月 24 日 下午 5:33

  3. Da Scritch

    这是一篇关于 WebComponents 的清晰而精彩的介绍,这是 Firefox 中一项期待已久的功能。

    我写了这篇论文(法语,抱歉)来解释我是如何用它来重新定义音频标签接口的 https://dascritch.net/post/2018/11/06/Reconstruire-son-lecteur-audio-pour-le-web
    论文的第一部分是网页中声音包含的历史,可能对每个人来说都不太有趣。

    2018 年 11 月 27 日 上午 6:23

  4. voracity

    不错。

    有没有办法从外部设置影子 DOM 中的内容的样式? 或者通过自定义元素的显式支持(假设我们有自定义元素)或没有支持?

    为什么没有自定义元素的支持? 因为有时,大约 90% 的组件布局是你想要的,但 10% 需要更改。 而且“有时”指的是 99% 的时间,包括内置到浏览器的组件……(期望组件作者考虑所有可能设置样式的内容也是不公平的,尤其考虑到浏览器制造商本身在历史上在这方面做得并不好。)

    2018 年 11 月 30 日 上午 4:11

本文的评论已关闭。