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

这不是一项简单的样式更改。 输入元素 是“无内容”元素,即没有结束标签的元素。 因此,没有地方可以将 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('input[type="file"]');
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = "<div>Hello from the other side</div>";
查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div
的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input
,但如果您遍历 功能边界
,那么它就不是 input
了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。
查看上面 Pen 中的结果。 这看起来像是更改一些 HTML 的冗长方法,但实际上并非如此。 如果您在浏览器中“检查元素”,您仍然会看到原始的文件输入 - 根本没有 div
的迹象。 更重要的是,如果您单击字符串,浏览器将弹出一个文件系统对话框。 这就是封装的关键:从外部来看,输入仍然是 input
,但如果您遍历 功能边界
,那么它就不是 input
了,而是一个字符串。 这意味着我们可以让输入(或任何其他标签)以我们想要的方式呈现。 我们可以将 Shadow Root 的内部 HTML 设置为任何有效的 HTML,它将按此呈现。 当然,包括添加 SVG。
var button = document.querySelector('input[type="file"]');
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = "<img src=\"https://s3-eu-west-1.amazonaws.com/chrisscott/codepen/iconmonstr-archive-7-icon.svg\" alt=\"Select file\"></img>";
查看上面 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("div");
var template = document.querySelector("template");
light.createShadowRoot().innerHTML = template.innerHTML;
document.querySelector("code").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
标签作为输入的子级插入。 您还记得,这是不允许的。 输入是一个空元素,并且没有允许的子元素,因此浏览器会将 img
和 style
元素作为无效子元素忽略。 这里有一个 使用 Polymer 平台的演示的 fork; 再次检查元素,您可以验证 polyfill 是否将模板添加为输入的子级。
因此,我们迎来了承诺的“一丝微弱的好消息”…… 尽管目前支持有限,但我仍然认为现在开始使用 Shadow DOM 来做这类事情是合理的。 为什么呢? 如果用户的浏览器不支持 Shadow DOM,它只会呈现一个普通的文件输入。 这不是一件坏事,考虑到复杂的(读作:hacky)替代方案,我认为 Shadow DOM 应该被视为一个可行的选择。
有关 Shadow DOM、Web Components 等的说明
Shadow DOM 和相关 Web Components 有着大量的用例。本文完全忽略了封装在开发框架和组件方面的优势。对于有兴趣的人来说,值得阅读 规范 并熟悉 Polymer。
一些关于 Web Components 的网站。
http://webcomponents.org/
http://customelements.io/
实际上,从 Chrome 35 开始,createShadowRoot 就可以在没有前缀的情况下使用。此外,Android KitKat 浏览器也支持它,但带有前缀。
如果你有 3 个文件输入,或者动态添加它们,你需要为每个文件手动替换它们的 shadow dom 吗?
关于最顶部的“为什么”,它是否实际上是因为它是一个被替换的元素——来自操作系统级别?它是一个自闭合标签的事实实际上不应该起作用太多,对吧?
结束标签是它需要包含内容的要求的结果吗?也许应该提到这一点,以避免对这种非标准标记造成混淆。
否则,看起来很棒,期待未来有更好的 shadowDOM 支持。
这对我来说读起来像一个史诗般的叙事。
是一个空元素。它不应该像你在 shadowDom.innerHTML 中那样有一个结束标签
<img>
。如果你认为这无关紧要,那么你应该看看这个 http://www.colorglare.com/2014/02/03/to-close-or-not-to-close.html我不明白为什么如果你放了不止一个输入,看起来除了第一个之外,其他输入的模板都不会正确应用。
我做错什么了吗?我希望有多个文件输入按钮。这是我的笔:http://codepen.io/napy84/pen/jsyIf
我真的需要学习更多关于 Shadow DOM 的知识。