JavaScript 是怎样事情: Shadow DOM 的内部结构+怎样编写自力的组件!

这是特地探究 JavaScript 及其所构建的组件的系列文章的第 17 篇。

想浏览更多优良文章请猛戳GitHub博客,一年百来篇优良文章等着你!

假如你错过了前面的章节,能够在这里找到它们:

  1. JavaScript 是怎样事情的:引擎,运行时和挪用客栈的概述!
  2. JavaScript 是怎样事情的:深切V8引擎&编写优化代码的5个技能!
  3. JavaScript 是怎样事情的:内存治理+怎样处置惩罚4个罕见的内存走漏!
  4. JavaScript 是怎样事情的:事宜轮回和异步编程的兴起+ 5种运用 async/await 更好地编码体式格局!
  5. JavaScript 是怎样事情的:深切探究 websocket 和HTTP/2与SSE +怎样挑选准确的途径!
  6. JavaScript 是怎样事情的:与 WebAssembly比较 及其运用处景!
  7. JavaScript 是怎样事情的:Web Workers的构建块+ 5个运用他们的场景!
  8. JavaScript 是怎样事情的:Service Worker 的生命周期及运用处景!
  9. JavaScript 是怎样事情的:Web 推送关照的机制!
  10. JavaScript是怎样事情的:运用 MutationObserver 跟踪 DOM 的变化!
  11. JavaScript是怎样事情的:衬着引擎和优化其机能的技能!
  12. JavaScript是怎样事情的:深切收集层 + 怎样优化机能和平安!
  13. JavaScript是怎样事情的:CSS 和 JS 动画底层道理及怎样优化它们的机能!
  14. JavaScript的怎样事情的:剖析、笼统语法树(AST)+ 提拔编译速率5个技能!
  15. JavaScript是怎样事情的:深切类和继续内部道理+Babel和 TypeScript 之间转换!
  16. JavaScript是怎样事情的:存储引擎+怎样挑选适宜的存储API!

《JavaScript 是怎样事情: Shadow DOM 的内部结构+怎样编写自力的组件!》

概述

Web Components 是一套差别的手艺,许可你建立可重用的定制元素,它们的功用封装在你的代码以外,你能够在 Web 运用中运用它们。

Web组件由四部份构成:

  • Shadow DOM(影子DOM)
  • HTML templates(HTML模板)
  • Custom elements(自定义元素)
  • HTML Imports(HTML导入)

在本文中重要解说 Shadow DOM(影子DOM)

Shadow DOM 这款东西旨在构建基于组件的运用。因而,可为收集开辟中的罕见问题供应处理方案:

  • 断绝 DOM:组件的 DOM 是自力的(比方,document.querySelector() 不会返回组件 shadow DOM 中的节点)。
  • 作用域 CSS:shadow DOM 内部定义的 CSS 在其作用域内。款式划定规矩不会走漏,页面款式也不会渗透。
  • 组合:为组件设想一个声明性、基于标记的 API。
  • 简化 CSS – 作用域 DOM 意味着您能够运用简朴的 CSS 挑选器,更通用的 id/类称号,而无需忧郁定名争执。

Shadow DOM

本文假定你已熟习 DOM 及别的的 Api 的观点。假如不熟习,能够在这里浏览关于它的细致文章—— https://developer.mozilla.org…

暗影 DOM 只是一个平常的 DOM,除了两个区分:

  • 建立/运用的体式格局
  • 与页面其他部份有关的行动体式格局

    平常,你建立 DOM 节点并将其附加至其他元素作为子项。 借助于 shadow DOM,您能够建立作用域 DOM 树,该 DOM 树附加至该元素上,但与其自身真正的子项星散开来。这一作用域子树称为影子树。被附着的元素称为影子宿主。 您在影子中增加的任何项均将成为宿主元素的当地项,包括 <style>。 这就是 shadow DOM 完成 CSS 款式作用域的体式格局

平常,建立 DOM 节点并将它们作为子元素追加到另一个元素中。借助于 shadow DOM,建立一个作用域 DOM 树,附该 DOM 树附加到元素上,但它与现实的子元素是星散的。这个作用域的子树称为 影子树,被附着的元素称为影子宿主。向影子树增加的任何内容都将成为宿主元素的当地元素,包括 <style>,这就是 影子DOM 完成 CSS 款式作用域的体式格局。

建立 shadow DOM

影子根是附加到“宿主”元素的文档片断,元素经由过程附加影子根来获取其 shadow DOM。要为元素建立暗影 DOM,挪用 element.attachShadow() :

var header = document.createElement('header');
var shadowRoot = header.attachShadow({mode: 'open'});
var paragraphElement = document.createElement('p');

paragraphElement.innerText = 'Shadow DOM';
shadowRoot.appendChild(paragraphElement);

范例定义了元素列表,这些元素没法托管影子树,元素之所以在所选之列,其缘由以下:

  • 浏览器已为该元素托管其自身的内部 shadow DOM(<textarea><input>)。
  • 让元素托管 shadow DOM 毫无意义 (<img>)。

比方,以下要领行不通:

document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.

Light DOM

这是组件用户写入的标记。该 DOM 不在组件 shadow DOM 以内,它是元素的现实孩子。假定已建立了一个名为<extended-button> 的定制组件,它扩大了原生 HTML 按钮组件,此时愿望在个中增加图象和一些文本。代码以下:

<extended-button>
  <!-- the image and span are extended-button's light DOM -->
  <img src="boot.png" slot="image">
  <span>Launch</span>
</extended-button>

“extension -button” 是定义的定制组件,个中的 HTML 称为 Light DOM,该组件由用户自身增加。

这里的 Shadow DOM 是你建立的组件 extension-button。Shadow DOM是 组件的当地组件,它定义了组件的内部组织、作用域 CSS 和 封装完成细节。

扁平 DOM 树

浏览器将用户建立的 Light DOM 分发到 Shadow DOM,并对终究产物举行衬着。扁平树是终究在 DevTools 中看到的以及页面上呈衬着的对象。

<extended-button>
  #shadow-root
  <style>…</style>
  <slot name="image">
    <img src="boot.png" slot="image">
  </slot>
  <span id="container">
    <slot>
      <span>Launch</span>
    </slot>
  </span>
</extended-button>

模板 (Templates)

假如须要 Web 页面上反复运用雷同的标签组织时,最好运用某种范例的模板,而不是一遍又一各处反复雷同的组织。这在之前也是能够完成,然则 HTML <template> 元素(在当代浏览器中取得了很好的支撑)使它变得轻易很多。此元素及其内容不在 DOM 中衬着,但能够运用 JavaScript 引用它。

一个简朴的例子:

<template id="my-paragraph">
  <p> Paragraph content. </p>
</template>

这不会出如今页面中,直到运用 JavaScrip t引用它,然后运用以下体式格局将其追加到 DOM 中:

var template = document.getElementById('my-paragraph');
var templateContent = template.content;
document.body.appendChild(templateContent);

到目前为止,已有其他手艺能够完成相似的行动,然则,正如前面提到的,将其原生封装起来是异常好的,Templates 也有相称不错的浏览器支撑:

《JavaScript 是怎样事情: Shadow DOM 的内部结构+怎样编写自力的组件!》

模板自身是有效的,但它们与自定义元素配合会更好。 能够 customElement Api 能定义一个自定义元素,而且示知 HTML 剖析器怎样准确地组织一个元素,以及在该元素的属性变化时实行响应的处置惩罚。

让我们定义一个 Web 组件名为 <my-paragraph>,该组件运用之前模板作为它的 Shadow DOM 的内容:

customElements.define('my-paragraph',
 class extends HTMLElement {
   constructor() {
     super();

     let template = document.getElementById('my-paragraph');
     let templateContent = template.content;
     const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
  }
});

这里须要注重的症结点是,我们向影子根增加了模板内容的克隆,影子根是运用 Node.cloneNode() 要领建立的。

由于将其内容追加到一个 Shadow DOM 中,所以能够在模板中运用 <style> 元素的情势包括一些款式信息,然后将其封装在自定义元素中。假如只是将其追加到规范 DOM 中,它是没法事情。

比方,能够将模板变动成:

<template id="my-paragraph">
  <style>
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>Paragraph content. </p>
</template>

如今自定义组件能够如许运用:

<my-paragraph></my-paragraph>

<slot> 元素

模板有一些瑕玷,重如果静态内容,它不许可我们衬着变量/数据,好能够让我们根据平常运用的规范 HTML 模板的习气来编写代码。Slot 是组件内部的占位符,用户能够运用自身的标记来添补。让我们看看上面的模板怎样运用 slot

<template id="my-paragraph">
  <p> 
    <slot name="my-text">Default text</slot> 
  </p>
</template>

假如在标记中包括元素时没有定义插槽的内容,或许浏览器不支撑插槽,<my-paragraph> 就只展现文本 “Default text”

为了定义插槽的内容,应该在 <my-paragraph> 元素中包括一个 HTML 组织,个中的 slot 属性的值为我们定义插槽的称号:

<my-paragraph>
 <span slot="my-text">Let's have some different text!</span>
</my-paragraph>

能够插进去插槽的元素称为 Slotable; 当一个元素插进去一个插槽时,它被称为开槽 (slotted)。

注重,在上面的例子中,插进去了一个 <span> 元素,它是一个开槽元素,它有一个属性 slot,它即是 my-text,与模板中的 slot 定义中的 name 属性的值雷同。

在浏览器中衬着后,上面的代码将构建以下扁平 DOM 树:

<my-paragraph>
  #shadow-root
  <p>
    <slot name="my-text">
      <span slot="my-text">Let's have some different text!</span>
    </slot>
  </p>
</my-paragraph>

设定款式

运用 shadow DOM 的组件可经由过程主页来设定款式,定义其自身的款式或供应钩子(以 CSS 自定义属性的情势)让用户替代默许值。

组件定义的款式

作用域 CSS 是 Shadow DOM 最大的特征之一:

  • 外部页面的 CSS 挑选器不运用于组件内部
  • 组件内定义的款式不会影响页面的其他元素,它们的作用域是宿主元素

shadow DOM 内部运用的 CSS 挑选器在当地运用于组件现实上,这意味着我们能够再次运用大众vid/类名,而不必忧郁页面上其他地方的争执,最好做法是在 Shadow DOM 内运用更简朴的 CSS 挑选器,它们在机能上也不错。

看看在 #shadow-root 定义了一些款式的:

#shadow-root
<style>
  #container {
    background: white;
  }
  #container-items {
    display: inline-flex;
  }
</style>

<div id="container"></div>
<div id="container-items"></div>

上面例子中的一切款式都是#shadow-root的当地款式。运用<link>元素在#shadow-root中引入款式表,这些款式表也都属于当地的。

:host 伪类挑选器

运用 :host 伪类挑选器,用来挑选组件宿主元素中的元素 (相关于组件模板内部的元素)。

<style>
  :host {
    display: block; /* by default, custom elements are display: inline */
  }
</style>

当涉及到 :host 挑选器时,应该警惕一件事:父页面中的划定规矩具有比元素中定义的 :host 划定规矩具有更高的优先级,这许可用户从外部掩盖顶级款式。而且 :host 只在影子根目录下事情,所以你不能在Shadow DOM 以外运用它。

假如 :host(<selector>) 的函数情势与 <selector> 婚配,你能够指定宿主,关于你的组件而言,这是一个很好的要领,它可以让你基于宿主将对用户互动或状况的回响反映行动举行封装,或对内部节点举行款式设定:

<style>
  :host {
    opacity: 0.4;
  }
  
  :host(:hover) {
    opacity: 1;
  }
  
  :host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
  }
  
  :host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
  }
</style>

:host-context(<selector>)

:host-context(<selector>) 或其恣意父级与 <selector> 婚配,它将与组件婚配。 比方,在文档的元素上能够有一个用于示意款式主题 (theme) 的 CSS 类,而我们应该基于它来决议组件的款式。
比方,很多人都经由过程将类运用到 <html> 或 <body> 举行主题化:

<body class="lightheme">
  <custom-container>
  …
  </custom-container>
</body>

鄙人面的例子中,只有当某个先人元素有 CSS 类theme-light时,我们才会把background-color款式运用到组件内部的一切元素中:

:host-context(.theme-light) h2 {
  background-color: #eef;
}

/deep/

组件款式平常只会作用于组件自身的 HTML 上,我们能够运用 /deep/ 挑选器,来强迫一个款式对各级子组件的视图也见效,它不只作用于组件的子视图,也会作用于组件的内容。

鄙人面例子中,我们以一切的元素为目的,从宿主元素到当前元素再到 DOM 中的一切子元素:

:host /deep/ h3 {
  font-style: italic;
}

/deep/ 挑选器另有一个别号 >>>,能够恣意交替运用它们。

/deep/
>>> 挑选器只能被用在
仿真 (emulated)形式下。 这类体式格局是默许值,也是用得最多的体式格局。

从外部为组件设定款式

有几种要领可从外部为组件设定款式:最简朴的要领是运用标记称号作为挑选器,以下

custom-container {
  color: red;
}

外部款式比在 Shadow DOM 中定义的款式具有更高的优先级。

比方,假如用户编写挑选器:

custom-container {
  width: 500px;
}

它将掩盖组件的款式:

:host {
  width: 300px;
}

对组件自身举行款式化只能到此为止。然则假如人想要对组件的内部举行款式化,会发作什么情况呢?为此,我们须要 CSS 自定义属性。

运用 CSS 自定义属性建立款式钩子

假如组件的开辟者经由过程 CSS 自定义属性供应款式钩子,则用户可调解内部款式。其头脑相似于<slot>,但适用于款式。

看看下面的例子:

<!-- main page -->
<style>
  custom-container {
    margin-bottom: 60px;
     - custom-container-bg: black;
  }
</style>

<custom-container background>…</custom-container>

在其 shadow DOM 内部:

:host([background]) {
  background: var( - custom-container-bg, #CECECE);
  border-radius: 10px;
  padding: 10px;
}

在本例中,该组件将运用 black 作为背景值,由于用户指定了该值,不然,背景色彩将采纳默许值 #CECECE

作为组件的作者,是有义务让开辟人员相识他们能够运用的 CSS 定制属性,将其视为组件的大众接口的一部份。

在 JS 中运用 slot

Shadow DOM API 供应了运用 slot 和散布式节点的实用程序,这些实用程序在编写自定义元素时早晚派得上用处。

slotchange 事宜

slot 的散布式节点发作变化时,slotchange 事宜将触发。比方,假如用户从 light DOM 中增加/删除子元素。

var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange', function(e) {
  console.log('Light DOM change');
});

要看管对 light DOM 的其他范例的变动,能够在元素的组织函数中运用 MutationObserver。之前讨论过 MutationObserver 的内部组织以及怎样运用它

assignedNodes() 要领

有时候,相识哪些元素与 slot 相关联异常有效。挪用 slot.assignedNodes() 可检察 slot 正在衬着哪些元素。 {flatten: true} 选项将返回 slot 的备用内容(条件是没有散布任何节点)。

让我们看看下面的例子:

<slot name=’slot1’><p>Default content</p></slot>

假定这是在一个名为 <my-container> 的组件中。

看看这个组件的差别用法,以及挪用 assignedNodes() 的效果是什么:

在第一种情况下,我们将向 slot 中增加我们自身的内容:

<my-container>
  <span slot="slot1"> container text </span>
</my-container>

挪用 assignedNodes() 会取得 [<span slot= " slot1 " > container text </span>],注重,效果是一个节点数组。

在第二种情况下,将内容置空:

<my-container> </my-container>

挪用 assignedNodes() 的效果将返回一个空数组 []

在第三种情况下,挪用 slot.assignedNodes({flatten: true}),取得效果是: [<p>默许内容</p>]

另外,要接见 slot 中的元素,能够挪用 assignedNodes() 来检察元素分配给哪一个组件 slot

事宜模子

值得注重的是,当发作在 Shadow DOM 中的事宜冒泡时,会发作什么。

当事宜从 Shadow DOM 中触发时,其目的将会调解为保持 Shadow DOM 供应的封装。也就是说,事宜的目的从新举行了设定,因而这些事宜看起来像是来自组件,而不是来自 Shadow DOM 中的内部元素。

下面是从 Shadow DOM 流传出去的事宜列表(有些没有):

  • 聚焦事宜:blur、focus、focusin、focusout
  • 鼠标事宜:click、dblclick、mousedown、mouseenter、mousemove,等等
  • 滚轮事宜:wheel
  • 输入事宜:beforeinput、input
  • 键盘事宜:keydown、keyup
  • 组合事宜:compositionstart、compositionupdate、compositionend
  • 拖放事宜:dragstart、drag、dragend、drop,等等

自定义事宜

默许情况下,自定义事宜不会流传到 Shadow DOM 以外。假如愿望分配自定义事宜并使其流传,则须要增加 bubbles: truecomposed: true 选项。

让我们看看派发如许的事宜是什么样的:

var container = this.shadowRoot.querySelector('#container');
container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));

浏览器支撑

如愿望取得 shadow DOM 检测功用,请检察是不是存在 attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

《JavaScript 是怎样事情: Shadow DOM 的内部结构+怎样编写自力的组件!》

有史以来第一次,我们具有了实行恰当 CSS 作用域、DOM 作用域的 API 原语,而且有真正意义上的组合。 与自定义元素等其他收集组件 API 组合后,shadow DOM 供应了一种编写真正封装组件的要领,无需花多大的工夫或运用如 <iframe> 等陈腐的东西。

代码布置后能够存在的BUG没法及时晓得,预先为相识决这些BUG,花了大批的时候举行log 调试,这边顺便给人人引荐一个好用的BUG监控东西 Fundebug

你的点赞是我延续分享好东西的动力,迎接点赞!

迎接到场前端人人庭,内里会常常分享一些手艺资本。

《JavaScript 是怎样事情: Shadow DOM 的内部结构+怎样编写自力的组件!》

    原文作者:前端小智
    原文地址: https://segmentfault.com/a/1190000018033709
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞