HTML5 ShadowDOM & CustomElements

KeKeMars 关注

2015.12.09 15:20* 字数 1239 阅读 1626评论 2喜欢 2

Web组件由四部分组成

  • Template
  • Shadow DOM (Chrome Opera支持)
  • Custom Elements
  • Packaging

Shadow DOM 组成

Shadow DOM可以和一个根节点Shadow root关联, 该Shadow DOM元素称为Shadow Host内容不会被渲染, 而Shadow root内容会被渲染。

但是,内容不应该放进Shadow DOM内, 以便被搜索引擎 阅读器等访问到, 可重用部件无意义的标记应该放进Shadow DOM

Shadow DOM从展现中分离细节

内容在文档内;展现在 Shadow DOM 里。 当需要更新的时候,浏览器会自动保持它们的同步。

  1. <template>
  2. <style>
  3. ……
  4. </style>
  5. <div class="outer">
  6. <div class="boilerplate">
  7. Hi! My name is
  8. </div>
  9. <div class="name">
  10. <content></content>
  11. </div>
  12. </div>
  13. </template>
  14. <script>
  15. var shadow = document.querySelector('#nameTag').createShadowRoot();
  16. var template = document.querySelector('#nameTagTemplate');
  17. var clone = document.importNode(template.content, true);
  18. shadow.appendChild(clone);
  19. document.querySelector("#nameTag").textContent = "Shellie"
  20. </script>
  21. <div id="nameTag"></div>

通过select特性, 可以使用多个元素并控制投射元素

  1. <!-- Shadow DOM -->
  2. <div style="background: purple; padding: 1em;">
  3. <div style="color: red;">
  4. <content select=".first"></content>
  5. </div>
  6. <div style="color: yellow;">
  7. <content select="div"></content>
  8. </div>
  9. <div style="color: blue;">
  10. <content select=".email"></content>
  11. </div>
  12. </div>
  13. <!-- DOM -->
  14. <div id="nameTag">
  15. <div class="first">Bob</div>
  16. <div>B. Love</div>
  17. <div class="email">bob@</div>
  18. </div>

Shadow DOM 样式

Shadow DOM定义的CSS样式只在Shadow Root下生效, 样式被封装起来

样式化宿主元素(host element)

:host样式化Shadow DOM元素, 并且无法影响到Shadow DOM外的元素

  1. :host(x-bar:host) {
  2. /* 当宿主是 <x-bar> 元素时生效。 */
  3. }
  4. :host(.different:host) {
  5. /* 当宿主的类 <class="diffent"> 时生效。 */
  6. }
  7. :host:hover {
  8. /* 当鼠标放置到宿主上时生效。 */
  9. opacity: 1;
  10. }

^(Hat) 和 ^^(Cat)选择器

^ 连接符等价于后代选择器(例如 div p {...}),只不过它能跨越 一个 shadow 边界。
^^ 后代选择器能够跨越 任意数量的 shadow 边界。
querySelector()支持该选择器

可以通过 shadowdom 样式化原生HTML控件

  1. video ^ input[type="range"] {
  2. background: hotpink;
  3. }

插入点重置样式

  1. var root = document.querySelector('div').createShadowRoot();
  2. root.resetStyleInheritance = false;
  1. {
  2. reset-style-inheritance: true;
  3. }

在插入点, 选择是否继承上级样式(只影响可继承的样式)

::content 伪元素 穿过插入点来指定样式

  1. <div>
  2. <h3>Light DOM</h3>
  3. <section>
  4. <div>I'm not underlined</div>
  5. <p>I'm underlined in Shadow DOM!</p>
  6. </section>
  7. </div>
  8. <script>
  9. var div = document.querySelector('div');
  10. var root = div.createShadowRoot();
  11. root.innerHTML = '\
  12. <style>\
  13. h3 { color: red; }\
  14. content[select="h3"]::content > h3 {\
  15. color: green;\
  16. }\
  17. ::content section p {\
  18. text-decoration: underline;\
  19. }\
  20. </style>\
  21. <h3>Shadow DOM</h3>\
  22. <content select="h3"></content>\
  23. <content select="section"></content>';
  24. </script>

对于一个 ShadowRoot 或 <shadow> 插入点:reset-style-inheritance 意味着可继承的 CSS 属性在宿主元素处被设置为 initial,此时这些属性还没有对 shadow 中的内容生效。该位置称为上边界(upper boundary)。

对于 <content> 插入点:reset-style-inheritance 意味着在宿主的子元素分发到插入点之前,将可继承的 CSS 属性设置为 initial。该位置称为下边界(lower boundary)。

使用多个shadowdom

最近添加的树称为 younger tree。之前添加的树称为 older tree。

添加进宿主元素中的 shadow 树按照它们的添加顺序而堆叠起来,从最先加入的 shadow 树开始。最终渲染的是最后加入的 shadow 树。

如果一个 shadow 树中存在多个 <shadow> 插入点,那么仅第一个被确认,其余的被忽略。

"Shadow 插入点" (<shadow>) 作为占位符可以插入 ShadowDOM
普通插入点 (<content>) 作为占位符可以插入 普通DOM元素

如果一个元素托管着 Shadow DOM,你可以使用 .shadowRoot 来访问它的 youngest shadow root

如果不想别人乱动你的 shadow,那就将 .shadowRoot 重定义为 null:

  1. Object.defineProperty(host, 'shadowRoot', {
  2. get: function() { return null; },
  3. set: function(value) { }
  4. });

JS中构建 shadowdom

可以使用 HTMLContentElement 和 HTMLShadowElement 接口。
使用插入点从宿主元素中选择并"分发"到 shadow 树

无法遍历 <content> 中的 DOM。
.getDistributedNodes() 允许我们查询一个插入点的分布式节点:

  1. <div id="example4">
  2. <h2>Eric</h2>
  3. <h2>Bidelman</h2>
  4. <div>Digital Jedi</div>
  5. <h4>footer text</h4>
  6. </div>
  7. <template id="sdom">
  8. <header>
  9. <content select="h2"></content>
  10. </header>
  11. <section>
  12. <content select="div"></content>
  13. </section>
  14. <footer>
  15. <content select="h4:first-of-type"></content>
  16. </footer>
  17. </template>
  18. <script>
  19. var container = document.querySelector('#example4');
  20. var root = container.createShadowRoot();
  21. var t = document.querySelector('#sdom');
  22. var clone = document.importNode(t.content, true);
  23. root.appendChild(clone);
  24. var html = [];
  25. [].forEach.call(root.querySelectorAll('content'), function(el) {
  26. html.push(el.outerHTML + ': ');
  27. var nodes = el.getDistributedNodes();
  28. [].forEach.call(nodes, function(node) {
  29. html.push(node.outerHTML);
  30. });
  31. html.push('\n');
  32. });
  33. </script>

可以在分布式节点上调用它的 .getDestinationInsertionPoints() 来查看它被分发进了哪个插入点中

  1. <div id="host">
  2. <h2>Light DOM</h2>
  3. </div>
  4. <script>
  5. var container = document.querySelector('div');
  6. var root1 = container.createShadowRoot();
  7. var root2 = container.createShadowRoot();
  8. root1.innerHTML = '<content select="h2"></content>';
  9. root2.innerHTML = '<shadow></shadow>';
  10. var h2 = document.querySelector('#host h2');
  11. var insertionPoints = h2.getDestinationInsertionPoints();
  12. [].forEach.call(insertionPoints, function(contentEl) {
  13. console.log(contentEl);
  14. });
  15. </script>

Shadow DOM 可视化渲染工具:
Shadow DOM Visualizer

shadowdom 事件模型

事件会被重定向,使它看起来是从宿主元素上发出,而并非是 Shadow DOM 的内部元素。(event.path 来查看调整后的事件路径。)

以下事件永远无法越过 shadow 边界:

  • abort
  • error
  • select
  • change
  • load
  • reset
  • resize
  • scroll
  • selectstart

自定义元素

使用场景

  • 定义新的 HTML/DOM 元素
  • 基于其他元素创建扩展元素
  • 给一个标签绑定一组自定义功能
  • 扩展已有 DOM 元素的 API

注册新元素

document.registerElement() 可以创建一个自定义元素

  • 第一个参数是元素的标签名。这个标签名必须包括一个连字符(-)。
  • 第二个参数是一个(可选的)对象,用于描述该元素的 prototype。在这里可以为元素添加自定义功能(例如:公开属性和方法)。
  1. var XFoo = document.registerElement('x-foo', {
  2. prototype: Object.create(HTMLElement.prototype)
  3. });
  4. // 非全局创建新元素, 可以放置到自己的命名空间内
  5. var myapp = {};
  6. myapp.XFoo = document.registerElement('x-foo');
  7. // 扩展原生元素 要创建扩展自元素 B 的元素 A,元素 A 必须继承元素 B 的 prototype。
  8. var MegaButton = document.registerElement('mega-button', {
  9. prototype: Object.create(HTMLButtonElement.prototype)
  10. });
  11. // 以下方法为重载版本
  12. var megaButton = document.createElement('button', 'mega-button');
  13. // <button is="mega-button">

添加JS属性和方法

  1. var XFooProto = Object.create(HTMLElement.prototype);
  2. // 1. 为 x-foo 创建 foo() 方法
  3. XFooProto.foo = function() {
  4. alert('foo() called');
  5. };
  6. // 2. 定义一个只读的“bar”属性
  7. Object.defineProperty(XFooProto, "bar", {value: 5});
  8. // 3. 注册 x-foo 的定义
  9. var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
  10. // 4. 创建一个 x-foo 实例
  11. var xfoo = document.createElement('x-foo');
  12. // 5. 插入页面
  13. document.body.appendChild(xfoo);
  14. /* 更简洁的方式 */
  15. var XFoo = document.registerElement('x-foo', {
  16. prototype: Object.create(HTMLElement.prototype, {
  17. bar: {
  18. get: function() { return 5; }
  19. },
  20. foo: {
  21. value: function() {
  22. alert('foo() called');
  23. }
  24. }
  25. })
  26. });

生命周期回调方法

回调名称 调用时间点
createdCallback 创建元素实例
attachedCallback 向文档插入实例
detachedCallback 从文档中移除实例
attributeChangedCallback(attrName, oldVal, newVal) 添加,移除,或修改一个属性
  1. var proto = Object.create(HTMLElement.prototype);
  2. proto.createdCallback = function() {
  3. this.addEventListener('click', function(e) {
  4. alert('Thanks!');
  5. });
  6. this.innerHTML = "<b>I'm an x-foo!</b>";
  7. };
  8. proto.attachedCallback = function() {...};
  9. var XFoo = document.registerElement('x-foo', {prototype: proto});

用 Shadow DOM 封装内部实现

  • 一种隐藏内部实现的方法,从而将用户与血淋淋的实现细节隔离开。
  • 简单有效的样式隔离。

从 Shadow DOM 创建元素,跟创建一个渲染基础标记的元素非常类似,区别在于 createdCallback() 回调:

  1. var XFooProto = Object.create(HTMLElement.prototype);
  2. XFooProto.createdCallback = function() {
  3. // 1. 为元素附加一个 shadow root。
  4. var shadow = this.createShadowRoot();
  5. // 2. 填入标记。
  6. shadow.innerHTML = "<b>I'm in the element's Shadow DOM!</b>";
  7. };
  8. var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

从模板创建元素

  1. <template id="sdtemplate">
  2. <style>
  3. p { color: orange; }
  4. </style>
  5. <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
  6. </template>
  7. <script>
  8. var proto = Object.create(HTMLElement.prototype, {
  9. createdCallback: {
  10. value: function() {
  11. var t = document.querySelector('#sdtemplate');
  12. var clone = document.importNode(t.content, true);
  13. this.createShadowRoot().appendChild(clone);
  14. }
  15. }
  16. });
  17. document.registerElement('x-foo-from-template', {prototype: proto});
  18. </script>

为自定义元素增加样式

  1. <style>
  2. app-panel {
  3. display: flex;
  4. }
  5. [is="x-item"] {
  6. transition: opacity 400ms ease-in-out;
  7. opacity: 0.3;
  8. flex: 1;
  9. text-align: center;
  10. border-radius: 50%;
  11. }
  12. [is="x-item"]:hover {
  13. opacity: 1.0;
  14. background: rgb(255, 0, 255);
  15. color: white;
  16. }
  17. app-panel > [is="x-item"] {
  18. padding: 5px;
  19. list-style: none;
  20. margin: 0 7px;
  21. }
  22. </style>
  23. <app-panel>
  24. <li is="x-item">Do</li>
  25. <li is="x-item">Re</li>
  26. <li is="x-item">Mi</li>
  27. </app-panel>

为使用 Shadow DOM 的元素增加样式

使用 :unresolved 伪类避免无样式内容闪烁(FOUC)

使用 :unresolved 伪类避免无样式内容闪烁(FOUC)

注册后渐显的 <x-foo> 标签:

  1. <style>
  2. x-foo {
  3. opacity: 1;
  4. transition: opacity 300ms;
  5. }
  6. x-foo:unresolved {
  7. opacity: 0;
  8. }
  9. </style>

:unresolved 伪类只能用于 unresolved 元素,而不能用于继承自 HTMLUnkownElement 的元素

  1. <style>
  2. /* 给所有 unresolved 元素添加边框 */
  3. :unresolved {
  4. border: 1px dashed red;
  5. display: inline-block;
  6. }
  7. /* unresolved 元素 x-panel 的文本内容为红色 */
  8. x-panel:unresolved {
  9. color: red;
  10. }
  11. /* 定义注册后的 x-panel 文本内容为绿色 */
  12. x-panel {
  13. color: green;
  14. display: block;
  15. padding: 5px;
  16. display: block;
  17. }
  18. </style>
  19. <panel>
  20. I'm black because :unresolved doesn't apply to "panel".
  21. It's not a valid custom element name.
  22. </panel>
  23. <x-panel>I'm red because I match x-panel:unresolved.</x-panel>

历史和浏览器支持

检查 document.registerElement() 是否存在:

  1. function supportsCustomElements() {
  2. return 'registerElement' in document;
  3. }
  4. if (supportsCustomElements()) {
  5. // Good to go!
  6. } else {
  7. // Use other libraries to create components.
  8. }

参考

ShadowDOM的更多相关文章

  1. 神秘的 shadow-dom 浅析

    说到 shadow-dom 可能很多人会很陌生.但是其实我们肯定碰到过,本文主要想简单介绍下 shadow-dom.下面直接进入正文. shadow-dom 是什么 顾名思义, shadow-dom, ...

  2. 影子节点 shadowDOM

    示例: <video controls autoplay name="media"> <source id="mp4" src="t ...

  3. HTML5 ShadowDOM & CustomElements

    Web组件由四部分组成 Template Shadow DOM (Chrome Opera支持) Custom Elements Packaging Shadow DOM 组成 Shadow DOM可 ...

  4. web 自动化遇到 shadowDOM 节点你会操作吗?

    本文转载自: http://www.lemfix.com/topics/971 近期有同学在做web自动化的时候,发现页面上有些元素,在selenium中无法通过xpath来定位,各种原因找了半天,都 ...

  5. Web自动化遇到shadowDOM节点操作(还没试)

    近期有同学在做web自动化的时候,发现页面上有些元素,在selenium中无法通过xpath来定位,各种原因找了半天,都没找到解决方案. 最后发现元素在一个叫做shadow-root的节点下面. 如下 ...

  6. 【shadow dom入UI】web components思想如何应用于实际项目

    回顾 经过昨天的优化处理([前端优化之拆分CSS]前端三剑客的分分合合),我们在UI一块做了几个关键动作: ① CSS入UI ② CSS作为组件的一个节点而存在,并且会被“格式化”,即选择器带id前缀 ...

  7. Vue.js:轻量高效的前端组件化方案

    转发一篇尤老师对vue.js的介绍,了解vue.js的来龙去脉.不过现在已经是2.0了,也有添加一些新的东西,当然有些东西也改了. Vue.js:轻量高效的前端组件化方案 Vue.js 是我在2014 ...

  8. Vue.js:轻量高效的前端组件化方案(转载)

    摘要:Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star.本文将从各方面对Vue ...

  9. Web Component 文章

    周末无意中了解了Web Component的概念. http://blog.amowu.com/2013/06/web-components.html http://www.v2ex.com/t/69 ...

随机推荐

  1. css滚动条样式修改

    .activeMoreBankList{ height: 188px; overflow-y: auto;} /*滚动条样式*/.activeMoreBankList::-webkit-scrollb ...

  2. pom.xml配置引用项目时不生效

    1 在项目pom.xml配置中引用项目A,但是编译时,取提数引起是B: 2 原因是:[Java Build Path - Projects] 引用的还是老的项目B,删除该引用即可解决.

  3. python 深复制和浅复制

    https://www.python-course.eu/python3_deep_copy.php-------------------------------------------------- ...

  4. Linux系统下设置vi编辑器,tab键为4

    1.cd ~ 2.vi .exrc 3.set tabstop=4(保存并退出)即可

  5. PHP 反射API

    出处:http://blog.csdn.net/hguisu/article/details/7357421 PHP5添加了一项新的功能:Reflection.这个功能使得phper可以reverse ...

  6. Adversarial Auto-Encoders

    目录 Another Approach: q(z)->p(z) Intuitively comprehend KL(p|q) Minimize KL Divergence How to comp ...

  7. 标准sqlserver连接语句

    sqlserver左右全内连接 原始链接http://www.cnblogs.com/youzhangjin/archive/2009/05/22/1486982.html      连接条件可在FR ...

  8. TCP传输的三次握手四次挥手策略

    为了准确无误地数据送达目标处,TCP协议采用了三次握手策略.用TCP协议把数据包送出去后,TCP不会对传送后的情况置之不理,它一定会向对方确认是否成功送达.握手中使用了TCP的标志:SYN和ACK 发 ...

  9. Leetcode 150.逆波兰表达式求值

    逆波兰表达式求值 根据逆波兰表示法,求表达式的值. 有效的运算符包括 +, -, *, / .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 说明: 整数除法只保留整数部分. 给定逆波兰表达式总 ...

  10. LOJ#541. 「LibreOJ NOIP Round #1」七曜圣贤

    有一辆车一开始装了编号0-a的奶茶,现有m次操作,每次操作Pi在[-1,b),若Pi为一个未出现过编号的奶茶,就把他买了并装上车:若Pi为一个在车上的奶茶,则把他丢下车:否则,此次操作为捡起最早丢下去 ...