使用封装实现语义化标记

Avatar of Chris Coyier
Chris Coyier

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

以下是 Chris Scott 的客座文章。 Chris 为我们介绍了 Shadow DOM 的一个很好的用例。 作为设计师,我们可能希望以某种方式设置样式,但有时却不得不与 HTML、CSS 和 JS 作斗争才能实现。 即使这样,结果也可能是笨重的、hacky 的,并且不符合语义。 Shadow DOM 或许可以帮助我们摆脱这种困境,为我们提供一个新的空间来使用任何需要的 HTML(需要 20 个空元素?没问题!),而不会将这种混乱暴露给实际 DOM(这对可访问性、语义等来说很糟糕)。

文件输入以难以设置样式而闻名。 假设您希望使用 SVG 图标来替代默认的按钮和文件名样式。

onjective

这不是一项简单的样式更改。 输入元素 是“无内容”元素,即没有结束标签的元素。 因此,没有地方可以将 SVG 元素“放入”其中。 那么您将如何处理呢?

好吧,让我们采用渐进增强方法,从基本功能开始。

<form>
  <input type="file"></input>
</form>

我们开箱即用得到了什么? 从语义上来说,它是非常描述性的:有一个元素允许用户输入文件。 它也是功能性的:如果点击它,将打开一个文件系统对话框。

现在让我们考虑添加图标。 您可以将图像放在输入附近,也许使用 z 索引和透明输入来保留输入的功能(例如 这里)。 这种方法在语义上是可以的(您有一个输入一张图像),但它确实感觉很 hacky,而且更难测试。 或者,您可以使用一个 <img> 标签,并完全放弃 <input>,但这样您就需要处理所有复制文件输入功能的问题,并且您已经丢失了标记的大部分含义。

(还有其他方法可以做到这一点。 例如,带有隐藏输入的标签,但为了切入正题,让我们继续前进。)

Shadow DOM

我认为,更好的方法是使用 Shadow DOM。 Shadow DOM 是 Web Components 的组成部分之一,您可以在 这里 阅读有关所有内容的信息。 我只想谈谈它的语义优势,暂时先这样。

Shadow DOM 在 W3C 草案规范 中被描述为 功能封装功能边界。 这意味着我们可以封装一些设计并将其固定到 DOM 树上的现有节点(称为 Shadow Root)。 回到我们的例子

var button = document.querySelector(&#x27;input[type=&quot;file&quot;]&#x27;);
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = &quot;&lt;div&gt;Hello from the other side&lt;/div&gt;&quot;;

查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div 的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input,但如果您遍历 功能边界,那么它就不是 input 了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。

查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div 的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input,但如果您遍历 功能边界,那么它就不是 input 了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。

var button = document.querySelector(&#x27;input[type=&quot;file&quot;]&#x27;);
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = &quot;&lt;img src=\&quot;https://s3-eu-west-1.amazonaws.com/chrisscott/codepen/iconmonstr-archive-7-icon.svg\&quot; alt=\&quot;Select file\&quot;&gt;&lt;/img&gt;&quot;;

查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div 的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input,但如果您遍历 功能边界,那么它就不是 input 了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。

显然,这不是一个特别好的替代方案,但这是一个开始。 并且 HTML 是简单且语义正确的。 要进一步了解这一点,让我们看一下 HTML5 Web Components 工具箱中的另一个工具,它对这种事情很有用:template 标签

模板

模板标签基本上是构建未渲染的 HTML 的一种方法。 模板的妙处在于,您可以使用 JavaScript 将来自模板的 DOM 注入到文档中其他地方的元素中。 在这种情况下,我们可以为我们漂亮的矢量图像文件输入定义一个模板,并使用它来定义实际 input 标签的 Shadow DOM。 这是一个合适的模板。

<template id="file-button-template">
  <style>
    img {
      padding: 6px;
      border: 1px solid grey;
      border-radius: 4px;
      box-shadow: 1px 1px 4px grey;
      background-color: lightgrey;
      width: 30px;
      cursor: pointer;
    }
    img:hover {
      background-color: whitesmoke;
    }
  </style>
  <img src="https://s3-eu-west-1.amazonaws.com/chrisscott/codepen/iconmonstr-archive-7-icon.svg" alt="Select file"></img>
</template>

将所有内容整合在一起

template 元素可以放置在文档的 head 中,我认为这是一个不错的位置。 以下 Pen 中的模板是我们刚刚在 head 中定义的。 如果您 在 CodePen.io 上打开它,您就可以通过单击 HTML 面板左上角的齿轮来查看模板。

查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div 的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input,但如果您遍历 功能边界,那么它就不是 input 了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。

我认为效果不错。 从语义上来说,它与原始 HTML 完全相同,但在视觉上是完全定制的。

浏览器支持、polyfill 和优雅降级

在浏览器对 Shadow DOM 的支持方面,有一些坏消息(然后更多坏消息,最后是一丝微弱的好消息)。

第一个坏消息是,目前,只有在 WebKit/Blink 浏览器中才支持编辑 Shadow DOM,使用带前缀的 element.webkitCreateShadowRoot() 方法。 不过,Firefox 30 应该也支持 Shadow DOM

第二个坏消息:Polyfils 可能不支持这种用例 - 当然 Polymer 也不支持。 在 Polymer 平台的情况下,要理解为什么 polyfill 的行为与本机实现不同,需要理解 polyfill 的机制。 本质上,Polymer 平台通过使用包装器替换本机 DOM 节点来执行封装。 这些包装器模拟了实际 DOM 节点的行为; 例如,您可以像设置本机节点一样设置包装器的 innerHTML。 包装器维护所谓的 light DOM 和 shadow DOM。 就封装而言,这工作得很好,因为包装器可以拦截 light 侧的调用(例如 children getter)并隐藏 shadow DOM。

var light = document.querySelector(&quot;div&quot;);
var template = document.querySelector(&quot;template&quot;);

light.createShadowRoot().innerHTML = template.innerHTML;

document.querySelector(&quot;code&quot;).innerHTML = 
  light.innerHTML

查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div 的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input,但如果您遍历 功能边界,那么它就不是 input 了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。

在这个例子中,检索到的“light” DOM 的内部 HTML 显示了封装正在工作(没有 h1 标签)。 然而,polyfill 并没有使用实际的 Shadow DOM(否则它就不是 polyfill 了!),您可以通过检查结果中的第一个 div 来验证这一点。 在 DOM 中,您会看到,存在一个 h1 标签 - 它没有隐藏在 Shadow DOM 中,而是就在那里。 这是因为,在包装器之下,Polymer 平台只是在操纵常规 DOM 树,因为不存在 shadow root。

就文件输入 SVG 图标而言,情况并不好。 如果使用 Polymer polyfill 运行文件输入的演示,就像之前的示例一样,它只会将 img 标签作为输入的子级插入。 您还记得,这是不允许的。 输入是一个元素,并且没有允许的子元素,因此浏览器会将 imgstyle 元素作为无效子元素忽略。 这里有一个 使用 Polymer 平台的演示的 fork; 再次检查元素,您可以验证 polyfill 是否将模板添加为输入的子级。

因此,我们迎来了承诺的“一丝微弱的好消息”…… 尽管目前支持有限,但我仍然认为现在开始使用 Shadow DOM 来做这类事情是合理的。 为什么呢? 如果用户的浏览器不支持 Shadow DOM,它只会呈现一个普通的文件输入。 这不是一件坏事,考虑到复杂的(读作:hacky)替代方案,我认为 Shadow DOM 应该被视为一个可行的选择。

有关 Shadow DOM、Web Components 等的说明

Shadow DOM 和相关 Web Components 有着大量的用例。本文完全忽略了封装在开发框架和组件方面的优势。对于有兴趣的人来说,值得阅读 规范 并熟悉 Polymer