前端基础知识学习之-ShadowDOM&LightDOM

73 篇文章 1 订阅
24 篇文章 0 订阅

DOM

DOM和SAX

W3C制定了一套书写XML分析器的标准接口规范——DOM。

除此以外,XML_DEV邮件列表中的成员根据应用的需求也自发地定义了一套对XML文档进行操作的接口规范——SAX。

这两种接口规范各有侧重,互有长短,应用都比较广泛。

应用程序开发过程中,应用程序不是直接对XML文档进行操作的,而是首先由XML分析器对XML文档进行分析。

然后,应用程序通过XML分析器所提供的DOM接口或SAX接口对分析结果进行操作,从而间接地实现了对XML文档的访问。

DOM介绍 优缺点分析

DOM: Document Object Model 文档对象模型。

在应用程序中,基于DOM的XML分析器将一个XML文档转换成一个对象模型的集合(通常称DOM树),应用程序通过对这个对象模型的操作,来实现对XML文档数据的操作。

通过DOM接口,应用程序可以在任何时候访问XML文档中的任何一部分数据,因此,这种利用DOM接口的机制也被称作随机访问机制。

DOM树所提供的随机访问方式给应用程序的开发带来了很大的灵活性,它可以任意地控制整个XML文档中的内容。

然而,由于DOM分析器把整个XML文档转化成DOM树放在了内存中,因此,当文档比较大或结构比较复杂时,对内存的需求就比较高。

而且,对于结构复杂的树的遍历也是一项耗时的操作。

所以,DOM分析器对机器性能的要求比较高,实现效率不十分理想。

由于DOM分析器所采用的树结构的思想与XML文档的结构相吻合,同时鉴于随机访问所带来的方便,因此,DOM分析器还是有很广泛的应用价值的。

DOM的组成

对于XML应用开发来说,DOM就是一个对象化的XML数据接口,一个与语言无关、与平台无关的标准接口规范。

DOM定义了HTML文档和XML文档的逻辑结构,给出了一种访问和处理这两种文档的方法。

文档代表的是数据,而DOM则代表了如何去处理这些数据。

作为W3C的标准接口规范,目前,DOM由三部分组成,包括:核心(core)、HTML接口和XML接口。

核心部分是结构化文档比较底层对象的集合,这一部分所定义的对象已经完全可以表达出任何HTML和XML文档中的数据了。

HTML接口和XML接口两部分则是专为操作具体HTML文档和XML文档所提供的高级接口

  • DOM是一种将HTML/XML文档组织成对象模型的建模过程;
  • DOM建模重点在于如何解析HTML/XML文档和开放符合DOM接口规范的节点操作API接口。

一个由xml组成的DOM结构树如下

<?xml version="1.0" encoding="utf-8"?>

<bookstore>
    <book category="children">
          <title lang="en">Harry Potter</title> 
          <author>J K. Rowling</author> 
          <year>2005</year> 
          <price>29.99</price> 
    </book>

    <book category="cooking">
          <title lang="en">Everyday Italian</title> 
          <author>Giada De Laurentiis</author> 
          <year>2005</year> 
          <price>30.00</price> 
    </book>

    <book category="web">
          <title lang="en">Learning XML</title> 
          <author>Erik T. Ray</author> 
          <year>2003</year> 
          <price>39.95</price> 
    </book>

    <book category="web">
          <title lang="en">XQuery Kick Start</title> 
          <author>James McGovern</author> 
          <author>Per Bothner</author> 
          <author>Kurt Cagle</author> 
          <author>James Linn</author> 
         <author>Vaidyanathan Nagarajan</author> 
          <year>2003</year> 
          <price>49.99</price> 
    </book>

</bookstore>

DOM tree

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple DOM example</title>
  </head>
  <body>
      <section>
        <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth.">
        <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p>
      </section>
  </body>
</html>

生成如下的DOM结构树
在这里插入图片描述

DOM的四个基本接口
  在DOM接口规范中,有四个基本的接口:Document, Node, NodeList, NamedNodeMap

Document

Document接口是对文档进行操作的入口,它是从Node接口继承过来的。

Node

Node接口是其他大多数接口的父类。

在DOM树中,Node接口代表了树中的一个节点。

NodeList

NodeList接口是一个节点的集合,它包含了某个节点中的所有子节点。

它提供了对节点集合的抽象定义,并不包含如何实现这个节点集的定义。

NodeList用于表示有顺序关系的一组节点,比如某个节点的子节点序列。

在DOM中,NodeList的对象是live的,对文档的改变,会直接反映到相关的NodeList对象中。

NamedNodeMap

NamedNodeMap接口也是一个节点的集合,通过该接口,可以建立节点名和节点之间的一一映射关系,从而利用节点名可以直接访问特定的节点,这个接口主要用在属性节点的表示上。

尽管NamedNodeMap所包含的节点可以通过索引来进行访问,但是这只是提供了一种枚举方法,NamedNodeMap所包含的节点集中节点是无序的。

与NodeList相同,在DOM中,NamedNodeMap对象也是live的。

lightDOM & shadowDOM

封装是面向对象编程的基本特性,它使程序员能够限制对某些对象组件的未授权访问。

在此定义下,对象以公共访问方法的形式提供接口作为与其数据交互的方式。这样对象的内部表示不能直接被对象的外部访问。

Shadow DOM将此概念引入HTML。它允许你将隐藏的,分离的DOM链接到元素,这意味着你可以使用HTML和CSS的本地范围。现在可以用更通用的CSS选择器而不必担心命名冲突,并且样式不再泄漏或被应用于不恰当的元素。

实际上,Shadow DOM API正是库和小部件开发人员将HTML结构、样式和行为与代码的其他部分分开所需的东西。

Shadow root 是 shadow 树中最顶层的节点,是在创建 shadow DOM 时被附加到常规DOM节点的内容。具有与之关联的shadow root的节点称为shadow host。

你可以像使用普通DOM一样将元素附加到shadow root。链接到shadow root的节点形成 shadow 树。通过图表应该能够表达的更清楚:

light dom&shadow dom

术语light DOM通常用于区分正常DOM和shadow DOM。shadow DOM和light DOM被并称为逻辑DOM。light DOM与shadow DOM分离的点被称为阴影边界。 DOM查询和CSS规则不能到达阴影边界的另一侧,从而创建封装。

创建一个shadow DOM

要创建shadow DOM,需要用Element.attachShadow()方法将shadow root附加到元素:

var shadowroot = element.attachShadow(shadowRootInit);

来看一个简单的例子:

<div id="host"><p>Default text</p></div>
    
<script>
  const elem = document.querySelector('#host');
     
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
     
  // create a <p> element
  const p = document.createElement('p');
     
  // add <p> to the shadow DOM
  shadowRoot.appendChild(p);
     
  // add text to <p> 
  p.textContent = 'Hello!';
</script>

此代码将一个shadow DOM树附加到div元素,其id是host。这个树与div的实际子元素是分开的,添加到它之上的任何东西都将是托管元素的本地元素。

Chrome DevTools中的 Shadow root。

注意#host中的现有元素是如何被shadow root替换的。不支持shadow DOM的浏览器将使用默认内容。

现在,在将CSS添加到主文档时,样式规则不会影响shadow DOM:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  // set the HTML contained within the shadow root
  shadowRoot.innerHTML = '<p>Shadow DOM</p>';
</script>
 
<style>
  p {color: red}
</style>

在light DOM中定义的样式不能越过shadow边界。因此,只有light DOM中的段落才会变为红色。
light dom & shadow dom

相反,你添加到shadow DOM的CSS对于hosting元素来说是本地的,不会影响DOM中的其他元素:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {color: red}</style>`;
 
</script>

在这里插入图片描述
你还可以将样式规则放在外部样式表中,如下所示:

shadowRoot.innerHTML = `
  <p>Shadow DOM</p>
  <link rel="stylesheet" href="style.css">`;

要获取 shadowRoot 附加到的元素的引用,使用host属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(shadowRoot.host);    // => <div id="host"></div>
</script>

要执行相反操作并获取对元素托管的shadow root的引用,可以用元素的shadowRoot属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(elem.shadowRoot);    // => #shadow-root (open)
</script>

shadowRoot mode

当调用Element.attachShadow()方法来附加shadow root时,必须通过传递一个对象作为参数来指定shadow DOM树的封装模式,否则将会抛出一个TypeError。该对象必须具有mode属性,其值为 open 或 closed。

打开的shadow root允许你使用host元素的shadowRoot属性从root外部访问shadow root的元素,如下例所示:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach an open shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
  // Nodes of an open shadow DOM are accessible
  // from outside the shadow root
  elem.shadowRoot.querySelector('p').innerText = 'Changed from outside the shadow root';
  elem.shadowRoot.querySelector('p').style.color = 'red';
</script>

在这里插入图片描述
但是如果mode属性的值为“closed”,则尝试从root外部用JavaScript访问shadow root的元素时会抛出一个TypeError:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a closed shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
 
  elem.shadowRoot.querySelector('p').innerText = 'Now nodes cannot be accessed from outside';
  // => TypeError: Cannot read property 'querySelector' of null 
</script>

当mode设置为closed时,shadowRoot属性返回null。因为null值没有任何属性或方法,所以在它上面调用querySelector()会导致TypeError。浏览器通常用关闭的 shadow roo 来使某些元素的实现内部不可访问,而且不可从JavaScript更改。

要确定shadow DOM是处于open还是closed模式,你可以参考shadow root的mode属性:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  console.log(shadowRoot.mode);    // => closed
</script>

从表面上看,对于不希望公开其组件的shadow root 的Web组件作者来说,封闭的shadow DOM看起来非常方便,然而在实践中绕过封闭的shadow DOM并不难。通常完全隐藏shadow DOM所需的工作量超过了它的价值。

并非所有HTML元素都可以托管shadow DOM
只有一组有限的元素可以托管shadow DOM。下表列出了支持的元素:

+----------------+----------------+----------------+
|    article     |      aside     |   blockquote   |
+----------------+----------------+----------------+
|     body       |       div      |     footer     |
+----------------+----------------+----------------+
|      h1        |       h2       |       h3       |
+----------------+----------------+----------------+
|      h4        |       h5       |       h6       |
+----------------+----------------+----------------+
|    header      |      main      |      nav       |
+----------------+----------------+----------------+
|      p         |     section    |      span      |
+----------------+----------------+----------------+

尝试将shadow DOM树附加到其他元素将会导致“DOMException”错误。例如:

document.createElement('img').attachShadow({mode: 'open'});    
// => DOMException

用元素作为shadow host是不合理的,因此这段代码抛出错误并不奇怪。你可能会收到DOMException错误的另一个原因是浏览器已经用该元素托管了shadow DOM。

浏览器自动将shadow DOM附加到某些元素

Shadow DOM已存在很长一段时间了,浏览器一直用它来隐藏元素的内部结构,比如,和。

当你在HTML中使用元素时,浏览器会自动将shadow DOM附加到包含默认浏览器控件的元素。但DOM中唯一可见的是元素本身:

clipboard.png

要在Chrome中显示此类元素的shadow root,请打开Chrome DevTools设置(按F1),然后在“elements”部分下方选中“Show user agent shadow DOM”:

clipboard.png

选中“Show user agent shadow DOM”选项后,shadow root节点及其子节点将变为可见。以下是启用此选项后相同代码的显示方式:

clipboard.png

在自定义元素上托管shadow DOM

Custom Elements API 创建的自定义元素可以像其他元素一样托管shadow DOM。请看以下示例:

<my-element></my-element>
<script>
  class MyElement extends HTMLElement {
    constructor() {
 
      // must be called before the this keyword
      super();
 
      // attach a shadow root to <my-element>
      const shadowRoot = this.attachShadow({mode: 'open'});
 
      shadowRoot.innerHTML = `
        <style>p {color: red}</style>
        <p>Hello</p>`;
    }
  }
 
  // register a custom element on the page
  customElements.define('my-element', MyElement);
</script>

此代码了创建一个托管shadow DOM的自定义元素。它调用了customElements.define()方法,元素名称作为第一个参数,类对象作为第二个参数。该类扩展了HTMLElement并定义了元素的行为。

在构造函数中,super()用于建立原型链,并且把Shadow root附加到自定义元素。当你在页面上使用时,它会创建自己的shadow DOM:

clipboard.png

请记住,有效的自定义元素不能是单个单词,并且名称中必须包含连字符( - )。例如,myelement不能用作自定义元素的名称,并会抛出 DOMException 错误。

样式化host元素

通常,要设置host元素的样式,你需要将CSS添加到light DOM,因为这是host元素所在的位置。但是如果你需要在shadow DOM中设置host元素的样式呢?

这就是host()伪类函数的用武之地。这个选择器允许你从shadow root中的任何地方访问shadow host。这是一个例子:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host {
        display: inline-block;
        border: solid 3px #ccc;
        padding: 0 15px;
      }
    </style>`;
</script>

值得注意的是:host仅在shadow root中有效。还要记住,在shadow root之外定义的样式规则比:host中定义的规则具有更高的特殊性。

例如,#host { font-size: 16px; }的优先级高于 shadow DOM:host { font-size: 20px; }。实际上这很有用,这允许你为组件定义默认样式,并让组件的用户覆盖你的样式。唯一的例外是!important规则,它在shadow DOM中具有特殊性。

你还可以将选择器作为参数传递给:host(),这允许你仅在host与指定选择器匹配时才会定位host。换句话说,它允许你定位同一host的不同状态:

<style>
  :host(:focus) {
    /* style host only if it has received focus */
  }
 
  :host(.blue) {
    /* style host only if has a blue class */
  }
 
  :host([disabled]) {
    /* style host only if it's disabled */
  }
</style>

基于上下文的样式

要选择特定祖先内部的shadow root host ,可以用:host-context()伪类函数。例如:

:host-context(.main) {
  font-weight: bold;
}

只有当它是.main的后代时,此CSS代码才会选择shadow host :

<body class="main">
  <div id="host">
  </div>
</body>

:host-context()对主题特别有用,因为它允许作者根据组件使用的上下文对组件进行样式设置。

样式钩子

shadow DOM的一个有趣地方是它能够创建“样式占位符”并允许用户填充它们。这可以通过使用CSS自定义属性来完成。我们来看一个简单的例子:

<div id="host"></div>
 
<style>
  #host {--size: 20px;}
</style>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {font-size: var(--size, 16px);}</style>`;
 
</script>

这个shadow DOM允许用户覆盖其段落的字体大小。使用自定义属性表示法( — size: 20px)设置该值,并且shadow DOM用var()函数(font-size: var( — size, 16px))检索该值。在概念方面,这类似于元素的工作方式。

可继承的样式

shadow DOM允许你创建独立的DOM元素,而不会从外部看到选择器可见性,但这并不意味着继承的属性不会通过shadow边界。

某些属性(如color,background和font-family)会传递shadow边界并应用于shadow树。因此,与iframe相比,shadow DOM不是一个非常强大的障碍。

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
</script>

解决方法很简单:通过声明all: initial将可继承样式重置为其初始值,如下所示:

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host p {
        all: initial;
      }
    </style>`;
</script>

在此例中,元素被强制回到初始状态,因此穿过shadow边界的样式不起作用。

重新定位事件

在shadow DOM内触发的事件可以穿过shadow边界并冒泡到light DOM;但是,Event.target的值会自动更改,因此它看起来好像该事件源自其包含的shadow树而不是实际元素的host元素。

此更改称为事件重定向,其背后的原因是保留shadow DOM封装。请参考以下示例:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    <ul>
    `;
 
  document.addEventListener('click', (event) => {
    console.log(event.target);
  }, false);
</script>

当你单击shadow DOM中的任何位置时,这段代码会将

记录到控制台,因此侦听器无法看到调度该事件的实际元素。

但是在shadow DOM中不会发生重定目标,你可以轻松找到与事件关联的实际元素:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    </ul>`;
   
  shadowRoot.querySelector('ul').addEventListener('click', (event) => {
    console.log(event.target);
  }, false);  
</script>

请注意,并非所有事件都会从shadow DOM传播出去。那些做的是重新定位,但其他只是被忽略了。如果你使用自定义事件的话,则需要使用composed:true标志,否则事件不会从shadow边界冒出来。

Shadow DOM v0 与 v1

Shadow DOM规范的原始版本在 Chrome 25 中实现,当时称为Shadow DOM v0。该规范的新版本改进了Shadow DOM API的许多方面。

例如,一个元素不能再承载多个shadow DOM,而某些元素根本不能托管shadow DOM。违反这些规则会导致错误。

此外,Shadow DOM v1提供了一组新功能,例如打开 shadow 模式、后备内容等。你可以找到由规范作者之一编写的 v0 和 v1 之间的全面比较(https://hayato.io/2016/shadow…)。可以在W3C找到Shadow DOM v1的完整描述。

浏览器对Shadow DOM v1的支持

在撰写本文时,Firefox和Chrome已经完全支持Shadow DOM v1。不幸的是,Edge尚未实现v1,Safari 只是部分支持。在 Can I use…上提供了支持的浏览器的最新列表。

要在不支持Shadow DOM v1的浏览器上实现shadow DOM,可以用shadydom和shadycss polyfills。

总结

DOM开发中缺乏封装一直是个问题。 Shadow DOM API为我们提供了划分DOM范围的能力,从而为这个问题提供了一个优雅的解决方案。

现在,样式冲突不再是一个令人担忧的问题,选择器也不会失控。 shadow DOM改变了小部件开发的游戏规则,能够创建从页面其余部分封装的小部件,并且不受其他样式表和脚本的影响,这是一个巨大的优势。

如前所述,Web 组件由三个主要技术组成,而shadow DOM是其中的关键部分。希望在阅读本文之后,你将更容易理解这三种技术是如何协同构建Web组件的。

参考链接

DOM 是什么?
神秘的 shadow-dom 浅析
深入理解Shadow DOM v1
XML DOM解析 基础概念
MDN Shadow DOM

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值