Web Components 指南

Avatar of Rob Dodson
Rob Dodson 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下是 Rob Dodson (@rob_dodson) 的客座文章。Rob 和我在 CodePen 支持上反复讨论如何在他的演示中使用 Polymer(一种 Web Components 的 polyfill,有点像)。我们最终成功地实现了它,并且这件事演变成了这篇文章。Rob,你来吧。

更新:Rob 于 2014年3月5日 更新了这篇文章,使所有内容都保持最新,因为这项技术目前发展非常迅速。

更新:再次更新于 2014年9月9日


最近,我与一位客户合作,培训其内部团队如何构建 Web 应用程序。在此过程中,我意识到我们目前构建前端的方式非常奇怪,甚至有点问题。在很多情况下,您要么从某个文档中复制大量 HTML 代码,然后将其粘贴到您的应用程序中(Bootstrap、Foundation 等),要么在页面上散布一些必须使用 JavaScript 配置的 jQuery 插件。这让我们处于一个相当不幸的境地,不得不选择臃肿的 HTML 或神秘的 HTML,而且通常我们两者都选择。

在理想情况下,HTML 语言应该足够表达性,能够创建复杂的 UI 小部件,并且还应该具有可扩展性,以便我们开发人员可以使用自己的标签来填补任何空白。今天,这终于可以通过一组名为 Web Components 的新标准实现了。

Web Components?

Web Components 是一组标准,它们正在通过 W3C 逐步完善并逐步应用于浏览器。简而言之,它们允许我们将标记和样式捆绑到自定义 HTML 元素中。这些新元素真正令人惊奇的是,它们完全封装了其所有 HTML 和 CSS。这意味着您编写的样式始终按预期呈现,并且您的 HTML 免受外部 JavaScript 的窥探。

如果您想使用原生 Web Components,我建议您使用 Chrome,因为它具有最好的支持。从 Chrome 36 版开始,它是第一个发布所有新标准的浏览器。

一个实际的例子

想想您目前是如何实现图像滑块的,它可能看起来像这样

<div id="slider">
  <input checked="" type="radio" name="slider" id="slide1" selected="false">
  <input type="radio" name="slider" id="slide2" selected="false">
  <input type="radio" name="slider" id="slide3" selected="false">
  <input type="radio" name="slider" id="slide4" selected="false">
  <div id="slides">
    <div id="overflow">
      <div class="inner">
        <img src="images//rock.jpg">
        <img src="images/grooves.jpg">
        <img src="images/arch.jpg">
        <img src="images/sunset.jpg">
      </div>
    </div>
  </div>
  <label for="slide1"></label>
  <label for="slide2"></label>
  <label for="slide3"></label>
  <label for="slide4"></label>
</div>

查看 Rob Dodson (@robdodson) 在 CodePen 上的笔 CSS3 滑块

图像滑块改编自 CSScience。图片由 Eliya Selhub 提供。

这是一大块 HTML 代码,我们甚至还没有包含 CSS!但是想象一下,如果我们可以去除所有这些额外的冗余代码,并将其减少到只有重要的部分。那会是什么样子?

<img-slider>
  <img src="images/sunset.jpg" alt="a dramatic sunset">
  <img src="images/arch.jpg" alt="a rock arch">
  <img src="images/grooves.jpg" alt="some neat grooves">
  <img src="images/rock.jpg" alt="an interesting rock">
</img-slider>

还不错!我们抛弃了样板代码,剩下的只有我们关心的东西。这就是 Web Components 将允许我们做的事情。但在深入探讨细节之前,我想再讲一个故事。

隐藏在阴影中

多年来,浏览器制造商一直袖藏着一个秘密技巧。看看这个 <video> 标签,认真思考一下,仅仅一行 HTML 代码就能获得所有这些视觉效果。

<video src="./foo.webm" controls></video>

有一个播放按钮、一个滑块、时间码和一个音量滑块。很多东西您不必编写任何标记,只要您请求 <video>,它就会出现。

但您实际看到的只是一个幻觉。浏览器制造商需要一种方法来保证他们实现的标签始终呈现相同,无论我们页面上可能已经存在哪些奇奇怪怪的 HTML、CSS 或 JavaScript。为此,他们创建了一条秘密通道,可以在其中隐藏他们的代码,并将其置于我们热切的双手之外。他们将这个秘密地方称为:Shadow DOM

如果您碰巧正在运行 Google Chrome,则可以打开开发者工具并启用 显示用户代理 Shadow DOM 标志。这将允许您更详细地检查 <video> 元素。

Enable Show Shadow DOM
Inspecting Show Shadow DOM

在里面,您会发现隐藏了大量 HTML 代码。足够长时间地四处查看,您会发现前面提到的播放按钮、音量滑块和各种其他元素。

现在,回想一下我们的图像滑块。如果我们可以访问 Shadow DOM 并且能够像 <video> 一样声明我们自己的标签呢?那么我们实际上就可以实现并使用我们自定义的 <img-slider> 标签了。

让我们看看如何实现这一点,使用 Web Components 的第一个支柱:模板。

模板

每个优秀的建筑项目都必须从蓝图开始,对于 Web Components,这个蓝图来自新的 <template> 标签。模板标签允许您在页面上存储一些标记,以后可以克隆和重用这些标记。如果您以前使用过像 mustache 或 handlebars 这样的库,那么 <template> 标签应该会感觉很熟悉。

<template>
  <h1>Hello there!</h1>
  <p>This content is top secret :)</p>
</template>

模板内的所有内容都被浏览器视为惰性。这意味着带有外部源的标签(<img><audio><video> 等)不会发出 http 请求,并且 <script> 标签不会执行。这也意味着模板内的任何内容都不会呈现到页面上,直到我们使用 JavaScript 激活它。

因此,创建 <img-slider> 的第一步是将所有 HTML 和 CSS 放入 <template> 中。

查看 Rob Dodson (@robdodson) 在 CodePen 上的笔 CSS3 滑块模板

完成此操作后,我们就可以将其移动到 Shadow DOM 中了。

Shadow DOM

为了真正确保我们的 HTML 和 CSS 不会对使用者产生负面影响,我们有时会求助于 iframe。它们确实可以解决问题,但您不希望在其中构建整个应用程序。

Shadow DOM 为我们提供了 iframe 最好的特性,即样式和标记封装,而几乎没有那么多的膨胀。

要创建 Shadow DOM,请选择一个元素并调用其 createShadowRoot 方法。这将返回一个文档片段,然后您可以用内容填充它。

<div class="container"></div>

<script>
  var host = document.querySelector('.container');
  var root = host.createShadowRoot();
  root.innerHTML = '<p>How <em>you</em> doin?</p>'
</script>

Shadow Host

在 Shadow DOM 术语中,您在其上调用 createShadowRoot 的元素被称为 Shadow Host。它是唯一对用户可见的部分,也是您要求用户向您的元素提供内容的地方。

如果您考虑我们之前提到的 <video> 标签,<video> 元素本身就是 Shadow Host,而内容是您在其内部嵌套的标签。

<video>
  <source src="trailer.mp4" type="video/mp4">
  <source src="trailer.webm" type="video/webm">
  <source src="trailer.ogv" type="video/ogg">
</video>

Shadow Root

createShadowRoot 返回的文档片段称为 Shadow Root。Shadow Root 及其后代对用户隐藏,但当浏览器看到我们的标签时,它实际上将呈现这些内容。

<video> 示例中,播放按钮、滑块、时间码等都是 Shadow Root 的后代。它们显示在屏幕上,但它们的标记对用户不可见。

阴影边界

阴影根内部的任何 HTML 和 CSS 都受到称为**阴影边界**的无形屏障的保护。阴影边界阻止父文档中的 CSS 渗透到阴影 DOM,并且还阻止外部 JavaScript 遍历到阴影根。

翻译:假设您在阴影 DOM 中有一个样式标签,该标签指定所有 h3 应具有红色的color。同时,在父文档中,您有一个样式,该样式指定 h3 应具有蓝色的color。在这种情况下,出现在阴影 DOM 内部的 h3 将为红色,而阴影 DOM 外部的 h3 将为蓝色。由于我们的朋友阴影边界,这两个样式将彼此忽略。

并且,如果在某个时刻,父文档开始查找带有$('h3')的 h3,则阴影边界将阻止对阴影根的任何探索,并且选择将仅返回阴影 DOM 外部的 h3。

这种级别的隐私是我们多年来梦寐以求并一直在努力解决的问题。说它将改变我们构建 Web 应用程序的方式是轻描淡写。

阴影滑块

要将我们的img-slider放入阴影 DOM,我们需要创建一个阴影宿主并用我们模板的内容填充它。

<template>
  <!-- Full of slider awesomeness -->
</template>

<div class="img-slider"></div>

<script>
  // Add the template to the Shadow DOM
  var tmpl = document.querySelector('template');
  var host = document.querySelector('.img-slider');
  var root = host.createShadowRoot();
  root.appendChild(document.importNode(tmpl.content, true));
</script>

在本例中,我们创建了一个div并为其指定了类img-slider,以便它可以充当我们的阴影宿主。

我们选择模板并使用document.importNode对其内部进行深度复制。然后将这些内部内容附加到我们新创建的阴影根。

如果您使用的是 Chrome,您实际上可以在以下笔中看到它的工作原理。

查看 Rob Dodson 的笔 CSS3 滑块阴影 DOM@robdodson)在 CodePen

插入点

此时,我们的img-slider位于阴影 DOM 中,但图像路径是硬编码的。就像嵌套在<video>内的<source>标签一样,我们希望图像来自用户,因此我们必须邀请他们从阴影宿主中获取。

要将项目拉入阴影 DOM,我们使用新的<content>标签。<content>标签使用 CSS 选择器从阴影宿主中挑选元素并将其投影到阴影 DOM 中。这些投影被称为**插入点**。

为了方便起见,我们假设滑块仅包含图像,这样我们就可以使用img选择器创建一个插入点。

<template>
  ...
  <div class="inner">
    <content select="img"></content>
  </div>
</template>

因为我们使用**插入点**将内容投影到 Shadow DOM 中,所以我们还需要使用新的::content伪元素来更新我们的 CSS。

#slides ::content img {
  width: 25%;
  float: left;
}

如果您想了解有关 Shadow DOM 添加的新 CSS 选择器和组合器的更多信息,请查看我整理的这份备忘单

现在我们准备填充我们的img-slider了。

<div class="img-slider">
  <img src="images/rock.jpg" alt="an interesting rock">
  <img src="images/grooves.jpg" alt="some neat grooves">
  <img src="images/arch.jpg" alt="a rock arch">
  <img src="images/sunset.jpg" alt="a dramatic sunset">
</div>

这真的很酷!我们减少了用户看到的标记数量。但为什么要止步于此?我们可以更进一步,将此img-slider变成它自己的标签。

自定义元素

创建您自己的 HTML 元素听起来可能令人生畏,但实际上非常简单。在 Web Components 中,这个新元素是一个**自定义元素**,并且只有两个要求:它的名称必须包含一个连字符,并且它的原型必须扩展HTMLElement

让我们看看它是如何工作的。

<template>
  <!-- Full of image slider awesomeness -->
</template>

<script>
  // Grab our template full of slider markup and styles
  var tmpl = document.querySelector('template');

  // Create a prototype for a new element that extends HTMLElement
  var ImgSliderProto = Object.create(HTMLElement.prototype);

  // Setup our Shadow DOM and clone the template
  ImgSliderProto.createdCallback = function() {
    var root = this.createShadowRoot();
    root.appendChild(document.importNode(tmpl.content, true));
  };

  // Register our new element
  var ImgSlider = document.registerElement('img-slider', {
    prototype: ImgSliderProto
  });
</script>

Object.create方法返回一个扩展HTMLElement的新原型。当解析器在文档中找到我们的标签时,它会检查它是否有一个名为createdCallback的方法。如果它找到此方法,它将立即运行它。这是一个进行设置工作的好地方,因此我们创建一些 Shadow DOM 并将我们的模板克隆到其中。

我们将标签名称和原型传递给document上的一个新方法,称为registerElement,之后我们就可以开始了。

现在我们的元素已注册,可以使用几种不同的方法。第一种也是最直接的方法是在我们的 HTML 中的某个地方使用<img-slider>标签。但我们也可以调用document.createElement("img-slider"),或者可以使用document.registerElement返回并存储在ImgSlider变量中的构造函数。您可以选择您喜欢的样式。

支持

构成 Web Components 的各种标准的支持令人鼓舞,并且一直在不断改进。此表说明了我们目前所处的位置。

Web Components Support Table

但是,不要因为某些浏览器不支持而阻止您使用它们!Mozilla 和 Google 的聪明人一直在努力构建 polyfill 库,这些库可以将对 Web Components 的支持偷偷带入**所有现代浏览器**!这意味着您今天就可以开始使用这些技术并向编写规范的人员提供反馈。该反馈非常重要,因此我们最终不会得到难用且难以使用的语法。

让我们看看如何使用 Google 的 Web Components 库Polymer重写我们的img-slider

Polymer 来救援!

Polymer 向浏览器添加了一个新标签<polymer-element>,该标签会自动将模板转换为阴影 DOM 并为我们注册自定义元素。我们只需要告诉 Polymer 使用什么名称作为标签,并确保我们包含了我们的模板标记。

查看 Chris Coyier 的笔 Polymer 滑块@chriscoyier)在 CodePen 上。

我发现使用 Polymer 创建元素通常更容易,因为库中内置了许多优点。这包括元素和模型之间的双向绑定、自动节点查找以及对 Web 动画等其他新标准的支持。此外,polymer-dev 邮件列表上的开发人员非常活跃且乐于助人,这在您初学时非常棒,并且StackOverflow 社区也在不断发展。

这只是 Polymer 可以做的事情的一个小例子,因此请务必访问其项目页面并查看 Mozilla 的替代方案X-Tag

问题

任何新标准都可能存在争议,在 Web Components 的情况下,它们似乎特别两极分化。在结束之前,我想就过去几个月我听到的一些反馈进行讨论,并给出我的看法。

天哪,它是 XML!!!

我认为当开发人员第一次看到自定义元素时,最让他们害怕的事情可能是它会将文档变成一大堆 XML 的想法,页面上的所有内容都具有某种定制的标签名称,并且以这种方式,我们将使网络几乎无法阅读。这是一个合理的论点,所以我决定踢蜂窝,并在Polymer 邮件列表中提出它

来回讨论很有意思,但我认为普遍共识是,我们只能通过实验来了解哪些方法有效,哪些无效。使用像<img-slider>这样的标签名是否更好、更语义化,或者我们目前的“div 汤”是唯一应该使用的方式?Alex Rusell撰写了一篇关于此主题的非常有见地的文章,我建议大家在做出决定之前花时间阅读一下。

SEO

目前尚不清楚爬虫对自定义元素和 Shadow DOM 的支持程度如何。Polymer 的常见问题解答中提到

搜索引擎已经处理了很长一段时间的基于 AJAX 的应用程序。远离 JS 并更具声明性是一件好事,并且通常会使事情变得更好。

Google 网站管理员博客最近宣布Google 爬虫会在索引页面之前执行页面上的 JavaScript。使用像Fetch as Google这样的工具,可以让你看到爬虫在解析你的网站时看到了什么。一个很好的例子是Polymer 网站,它使用自定义元素构建,并且可以在 Google 中轻松搜索。

我从与 Polymer 团队成员的交流中了解到的一点建议是,尽量确保自定义元素内的内容是静态的,而不是来自数据绑定。

<!-- probably good -->
<x-foo>
  Here is some interesting, and searchable content...
</x-foo>

<!-- probably bad -->
<x-foo>
  {{crazyDynamicContent}}
</x-foo>

<!-- also probably bad -->
<a href="{{aDynamicLink}}">Click here</a>

公平地说,这不是一个新问题。基于 AJAX 的网站已经处理这个问题几年了,值得庆幸的是,有一些解决方案

可访问性

显然,当你在秘密的 Shadow DOM 沙盒中隐藏标记时,可访问性问题变得非常重要。Steve Faulkner 研究了 Shadow DOM 中的可访问性,并且似乎对他的发现感到满意。

初步测试结果表明,在完全位于 Shadow DOM 内的内容中包含 ARIA 角色、状态和属性可以正常工作。可访问性信息通过可访问性 API 正确公开。屏幕阅读器可以无障碍地访问 Shadow DOM 中的内容。

完整文章可在此处获取。

Marcy Sutton* 也撰写了一篇帖子探讨了这个主题,她在其中解释道

Web Components(包括 Shadow DOM)是可访问的,因为辅助技术会将页面呈现为已渲染的页面,这意味着整个文档都被读取为“一棵快乐的树”。

*Marcy 还指出,我在本文中构建的 img-slider 无法访问,因为我们的 CSS 标签技巧使其无法通过键盘访问。如果你打算在项目中重用它,请记住这一点。

当然,在前进的道路上会遇到一些障碍,但这听起来是一个非常好的开始!

样式标签?嗯,不,谢谢。

不幸的是,<link>标签在 Shadow DOM 中不起作用,这意味着引入外部 CSS 的唯一方法是通过@import。换句话说,<style>标签——目前——是不可避免的。*

请记住,我们正在讨论的样式仅与组件相关,而我们之前已经接受过训练,倾向于使用外部文件,因为它们通常会影响我们的整个应用程序。因此,如果所有这些样式都只作用于一个实体,那么在元素内部放置一个<style>标签是不是一件坏事?我个人认为是可以的,但是如果能有外部文件的选项会非常好。

* 除非你使用 Polymer,它使用 XHR 绕过了此限制

现在轮到你了

由我们来决定这些标准应该走向何方,以及哪些最佳实践将指导它们。尝试一下Polymer,并查看 Mozilla 对 Polymer 的替代方案X-Tag(它支持一直到 Internet Explorer 9)。

此外,请务必联系Google 的开发人员Mozilla,他们正在推动这些标准的发展。我们需要你的反馈才能将这些工具塑造成我们都希望使用的工具。

虽然仍然存在一些不足之处,但我认为 Web Components 最终将带来一种新的应用程序开发风格,更类似于将乐高积木拼搭在一起,而不是我们目前的方法,后者往往充斥着过多的样板代码。我对这一切的走向感到非常兴奋,期待着未来可能带来的变化。