手记

HTML5 ShadowDOM & CustomElements

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 里。 当需要更新的时候,浏览器会自动保持它们的同步。

<template>
  <style>
    ……  </style>
  <div class="outer">
    <div class="boilerplate">
      Hi! My name is    </div>
    <div class="name">
      <content></content>
    </div>
  </div></template><script>
  var shadow = document.querySelector('#nameTag').createShadowRoot();  var template = document.querySelector('#nameTagTemplate');  var clone = document.importNode(template.content, true);
  shadow.appendChild(clone);  document.querySelector("#nameTag").textContent = "Shellie"</script><div id="nameTag"></div>

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

<!-- Shadow DOM --><div >
  <div >
    <content select=".first"></content>
  </div>
  <div >
    <content select="div"></content>
  </div>
  <div >
    <content select=".email"></content>
  </div></div><!-- DOM --><div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div></div>

Shadow DOM 样式

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

样式化宿主元素(host element)

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

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

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

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

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

video ^ input[type="range"] {  background: hotpink;
}

插入点重置样式

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

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

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

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section></div><script>var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = '\    <style>\
      h3 { color: red; }\
      content[select="h3"]::content > h3 {\
        color: green;\
      }\
      ::content section p {\
        text-decoration: underline;\
      }\    </style>\    <h3>Shadow DOM</h3>\    <content select="h3"></content>\    <content select="section"></content>';</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:

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

JS中构建 shadowdom

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

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

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4></div><template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer></template><script>var container = document.querySelector('#example4');var root = container.createShadowRoot();var t = document.querySelector('#sdom');var clone = document.importNode(t.content, true);
root.appendChild(clone);var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});</script>

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

<div id="host">
  <h2>Light DOM</h2></div><script>
  var container = document.querySelector('div');  var root1 = container.createShadowRoot();  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';  var h2 = document.querySelector('#host h2');  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {    console.log(contentEl);
  });</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。在这里可以为元素添加自定义功能(例如:公开属性和方法)。

var XFoo = document.registerElement('x-foo', {  prototype: Object.create(HTMLElement.prototype)
});// 非全局创建新元素, 可以放置到自己的命名空间内var myapp = {}; 
myapp.XFoo = document.registerElement('x-foo');// 扩展原生元素 要创建扩展自元素 B 的元素 A,元素 A 必须继承元素 B 的 prototype。var MegaButton = document.registerElement('mega-button', {  prototype: Object.create(HTMLButtonElement.prototype)
});// 以下方法为重载版本var megaButton = document.createElement('button', 'mega-button');// <button is="mega-button">

添加JS属性和方法

var XFooProto = Object.create(HTMLElement.prototype);// 1. 为 x-foo 创建 foo() 方法XFooProto.foo = function() {
  alert('foo() called');
};// 2. 定义一个只读的“bar”属性Object.defineProperty(XFooProto, "bar", {value: 5});// 3. 注册 x-foo 的定义var XFoo = document.registerElement('x-foo', {prototype: XFooProto});// 4. 创建一个 x-foo 实例var xfoo = document.createElement('x-foo');// 5. 插入页面document.body.appendChild(xfoo);/* 更简洁的方式 */var XFoo = document.registerElement('x-foo', {  prototype: Object.create(HTMLElement.prototype, {    bar: {      get: function() { return 5; }
    },    foo: {      value: function() {
        alert('foo() called');
      }
    }
  })
});

生命周期回调方法

回调名称调用时间点
createdCallback创建元素实例
attachedCallback向文档插入实例
detachedCallback从文档中移除实例
attributeChangedCallback(attrName, oldVal, newVal)添加,移除,或修改一个属性
var proto = Object.create(HTMLElement.prototype);

proto.createdCallback = function() {  this.addEventListener('click', function(e) {
    alert('Thanks!');
  });  this.innerHTML = "<b>I'm an x-foo!</b>";
};
proto.attachedCallback = function() {...};var XFoo = document.registerElement('x-foo', {prototype: proto});

用 Shadow DOM 封装内部实现

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

  • 简单有效的样式隔离。

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

var XFooProto = Object.create(HTMLElement.prototype);

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

从模板创建元素

<template id="sdtemplate">
  <style>
    p { color: orange; }  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p></template><script>var proto = Object.create(HTMLElement.prototype, {  createdCallback: {    value: function() {      var t = document.querySelector('#sdtemplate');      var clone = document.importNode(t.content, true);      this.createShadowRoot().appendChild(clone);
    }
  }
});document.registerElement('x-foo-from-template', {prototype: proto});</script>

为自定义元素增加样式

<style>
  app-panel {    display: flex;
  }
  [is="x-item"] {    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  [is="x-item"]:hover {    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > [is="x-item"] {    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-panel>
  <li is="x-item">Do</li>  <li is="x-item">Re</li>
  <li is="x-item">Mi</li>
</app-panel>

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

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

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

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

<style>
  x-foo {    opacity: 1;    transition: opacity 300ms;
  }  x-foo:unresolved {    opacity: 0;
  }</style>

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

<style>
  /* 给所有 unresolved 元素添加边框 */
  :unresolved {    border: 1px dashed red;    display: inline-block;
  }  /* unresolved 元素 x-panel 的文本内容为红色 */
  x-panel:unresolved {    color: red;
  }  /* 定义注册后的 x-panel 文本内容为绿色 */
  x-panel {    color: green;    display: block;    padding: 5px;    display: block;
  }</style><panel>
  I'm black because :unresolved doesn't apply to "panel".
  It's not a valid custom element name.</panel><x-panel>I'm red because I match x-panel:unresolved.</x-panel>

历史和浏览器支持

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

function supportsCustomElements() {  return 'registerElement' in document;
}if (supportsCustomElements()) {  // Good to go!} else {  // Use other libraries to create components.}

参考



作者:KeKeMars
链接:https://www.jianshu.com/p/10fce782397c


0人推荐
随时随地看视频
慕课网APP