Shadow DOM 内部组织及怎样构建自力组件

原文请查阅
这里,略有删减,本文采纳
学问同享签名 4.0 国际许可协定同享,BY
Troland

这是 JavaScript 事情道理的第十七章。

《Shadow DOM 内部组织及怎样构建自力组件》

概述

网页组件指的是许可开发者运用一系列差别的手艺来建立可复用的自定义元素,组件内的功用不影响别的代码,以便于开发者在网页顺序中运用。

有四种网页组件范例:

  • Shadow DOM
  • HTML 模板
  • 自定义元素
  • HTML Imports

本章主要议论 Shadow DOM

Shadow DOM 是一个被设想用来构建基于组件(积木式)的网页顺序的东西。它为开发者能够常常遇到过的题目供应相识决方案:

  • 断绝的 DOM:组件的 DOM 是自力的(比方 document.querySelector() 没法检索到组件 shadow DOM 下的f元素节点)。如许就能够简化网页顺序中的 CSS 挑选器,因为 DOM 组件是互不影响,如许就许可开发者能够为所欲为地运用越发通用的 id/class 定名而不必忧郁定名争执。
  • 部分款式: shadow DOM 内定义的款式不会污染 shadow DOM 以外的元素。Style 款式划定规矩不会走漏且页面款式也不会污染 shadow DOM 内的元素款式。
  • 组合:为开发者的组件设想一个声明式,基于标签的接口。

Shadow DOM

本篇文章假定开发者已对 DOM 及其 API 熟拈于心。不然,能够浏览一下这方面的详细资料

与平常的 DOM 元素比拟,Shadow DOM 有两处差别的处所:

  • 与平常建立和运用 DOM 的体式格局比拟,开发者怎样建立及运用 Shadow DOM 及其与页面上的别的元素的关联
  • 其展示情势与页面上的别的元素的关联

平常状况下,开发者建立 DOM 节点,然后将其作为子元素挂载到别的元素下。关于 shadow DOM,开发者建立一个自力 DOM 树挂载到目的元素下而该树和其现实子元素是星散的。该自力子树称为 shadow 树。shadow 树的挂载元素称为 shadow 宿主。包括 <style> 在内的一切在 shadow 树下建立的任何标签都只作用于宿主元素内部。此即 shadow DOM 怎样完成 CSS 部分款式化的道理。

建立 Shadow DOM

一个 shadow 根 等于一段挂载到 “宿主” 元素下的文档碎片。挂载了 shadow 根即示意宿主元素包括 shadow DOM。挪用 element.attachShadow() 要领来为元素建立 shadow DOM:

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

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

范例定义了不能够建立 shadow 树的元素列表。

Shadow DOM 组合功用

组合元素是 Shadow DOM 最主要的功用之一。

当誊写 HTML 的时刻,组合元素构建网页顺序。开发者组合及嵌套诸如 <div><header><form> 及别的差别的构建模块来构建网页顺序所需的界面。个中某些标签以至能够相互兼容。

元素组合定义了诸如为什么 <select><form><video> 及别的元素是可扩大的且接收特定的 HTML 元素作为子元素以便用来对这些元素举行特别处置惩罚。

比方,<select> 元素晓得怎样把 <option> 元素衬着成为带有预定义选项的下拉框组件。

Shadow DOM 引入以下功用,能够用来组合元素。

Light DOM

此即组件的誊写标记。该 DOM 存在于组件的 shadow DOM 以外。它是元素的现实子元素。假定开发者建立了一个名为 <better-button> 的自定义组件,扩大原生 button 标签及想在组件内部增加一个图片和一些文本。也许以下:

<extended-button>
  <!-- image 和 span 即为扩大 button 的 light DOM -->
  <img src="boot.png" slot="image">
  <span>Launch</span>
</extended-button>

「扩大 button」即开发者自定义组件,而个中的 HTML 即为 Light DOM 且是运用组件的用户所增加的。

这里的 Shadow DOM 即开发者建立的组件(「扩大 button」)。Shadow DOM 仅存在于组件内部且在个中定义其内部组织,部分款式及封装了组件完成概况。

扁平 DOM 树

浏览器分发 light DOM 的效果即,由用户在 Shadow DOM 内部建立的 HTML 内容,这些 HTML 内容构成了自定义组件的组织,衬着出末了的产物界面。扁平树即开发者在开发者东西中看到的内容和页面的衬着效果。

<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>

模板

当开发者不得不在网页上复用雷同的标记组织的时刻,最好运用某种模板而不是反复誊写雷同的页面组织。之前是能够完成的,然则如今能够运用 <template> (当代浏览器均兼容)元素轻易地完成该功用。该元素及其内容不会在 DOM 中衬着,然则能够运用 JavaScript 来援用个中的内容。

来看一个简朴示例:

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

上面的内容不会在页面中衬着,除非运用 JavaScript 来援用个中的内容,然后运用相似以下的代码来挂载到 DOM 中:

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

迄今为止,能够运用别的手艺来完成相似的功用,然则正如之前所提到的,只管运用原生功用来完成能够会更酷些。别的,兼容性也蛮好。

《Shadow DOM 内部组织及怎样构建自力组件》

本身模板就很好用,然则若和自定义元素合营运用会更好哦。我们将会别的的文章中引见自定义元素,当下开发者只需相识 customElement 接口许可开发者自定义标签内容的衬着。

让我们定义一个运用模板作为其 shadow DOM 衬着内容的网页组件。且称其为 <my-paragraph>:

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 根下。

别的,因为把模板的内容挂载到 shadow DOM 中,开发者能够在模板中运用 <style> 元素包括一些款式信息,该 <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>

插槽

模板有一些不足的处所,主要的不足在于静态内容不许可开发者像平常的范例 HTML 模板那样衬着自定义的变量或许数据。

这时刻 <slot> 就派上用场了。

能够把插槽看成是许可开发者在模板中安排自定义 HTML 的占位符的功用。如许开发者就能够建立能用的 HTML 模板而且经由过程引入插槽来自定义衬着内容。

让我们看一下以上模板增加一个插槽的代码以下:

<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>

一切能够被插进去插槽的元素被称为可插进去元素;已插进去插槽元素称为插槽元素。

注重以上示例中插进去的 <span> 元素等于插槽元素。它具有一个 slot 属性,属性值和模板中插槽定义的 name 属性值相称。

浏览器衬着以后,以上代码会建立以下扁平 DOM 树:

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

这里原文有误,有修正。

注重 #shadow-root 元素只是示意存在 Shadow DOM 罢了。

款式化

能够在主页面款式化含有 shadow DOM 的组件,能够定义组件款式或许供应 CSS 自定义属性的情势让用户掩盖掉默许款式值。

组件定义的款式

部分款式 是 Shadow DOM 极好的功用之一:

  • 主页面上的 CSS 挑选器不会影响到组件内部元素的款式。
  • 组件内部定义的款式不会影响页面上的别的元素款式。它们只作用于宿主元素。

Shadow DOM 中的 CSS 挑选器只影响组件内部的元素。现实上,这意味着开发者能够反复运用通用的 id/class 称号而不必忧郁和主页面上的别的款式发生争执。简朴的 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 内部。

开发者也能够在 #shadow-root 内里运用 <link> 元夙来引入款式表,也只作用于 #shadow-root 内部。

:host 伪类

:host 伪类许可开发者挑选和款式化包括 shadow 树的宿主元素:

<style>
  :host {
    display: block; /* 默许状况下, 自定义元素是内联元素 */
  }
</style>

只要一个处所须要注重即若主页面上定义的宿主元素款式优先级比元素内里定义的 :host 款式划定规矩要高。如许就许可开发者从外部掩盖掉组件内部定义的顶级款式。

即当在主页面上定义了以下的款式:

my-paragraph {
  marbin-bottom: 40px;
}

<template id="my-paragraph">
    <style>
        :host {
      margin-bottom: 30px;/* 将不起作用,因为会被前面父页面已定义的款式掩盖 */
        }
    </style>
  <p> 
    <slot name="my-text">Default text</slot> 
  </p>
</template>

同理,:host 只在shadow 根的高低文中起作用,因而开发者不能够在 Shadow DOM 表面运用。

:host(<selector>) 如许的功用款式许可开发者只款式化婚配 <selector> 的宿主元素。这是一个绝佳的体式格局,开发者能够在组件内部封装相运用户交互或许状况的行动,然后基于宿主元夙来款式化内部节点。

<style>
  :host {
    opacity: 0.4;
  }
  
  :host(:hover) {
    opacity: 1;
  }
  
  :host([disabled]) { /* 宿主元素具有 disabled 属性的款式. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
  }
  
  :host(.pink) > #tabs {
    color: pink; /* 当宿主元素含有 pink 类时的选项卡款式. */
  }
</style>

运用 :host-context(<selector>) 伪类来定制化元素款式

:host-context(<selector>) 伪类找出宿主元素或许宿主元素恣意的先人元素婚配 <selector>

常用于定制化。比方,开发者经由过程为 <html> 或许 <body> 增加类来举行定制化:

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

或许

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

当宿主元素的先人元素包括有 .lightheme 类 :host-context(.lightheme) 将会款式化 <fancy-tabs>

:host-context(.lightheme) {
  color: black;
  background: white;
}

能够运用 :host-context() 来举行定制化主题款式,然则更好的要领即经由过程 CSS 自定义属性来建立款式钩子。

从外部款式化组件宿主元素

开发者能够从外部经由过程把标签名作为挑选器来款式化组件宿主元素,以下:

custom-container {
  color: red;
}

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

比方,假定用户誊写以下挑选器:

custom-container {
  width: 500px;
}

将会掩盖以下组件款式划定规矩 :

:host {
  width: 300px;
}

组件本身款式化只能做到这么多。但假如想要款式化组件内部属性呢?这就须要 CSS 自定义属性。

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

若组件作者运用 CSS 自定义属性供应款式钩子,用户能够用来变动内部款式。

这和 <slot> 思绪相似只是运用到了款式。

让我们看以下示例:

<!-- 主页面 -->
<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;
}

该示例中,因为用户供应了该背景色彩值,所以组件将会把黑色作为背景色彩值。不然,默许为 #CECECE

作为组件作者,须要让开发者晓得能够运用的 CSS 自定义属性。能够把自定义属性看做组件的大众接口。

插槽 JavaScript 接口

Shadow DOM API 能够用来操纵插槽。

slotchange 事宜

当一个插槽的分发元素节点发生变化的时刻触发 slotchange 事宜。比方,当用户从 light DOM 中增加/删除子节点。

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

能够在元素的组织函数中建立 MutationObserver 来监听 light DOM 的别的范例的修正事宜。前面文章中有引见过 MutationObserver 的内部组织及运用指南

assignedNodes() 要领

相识哪些元素是和插槽有关是很有用途的。挪用 slot.assignedNodes() 能够找出哪些元素是由插槽衬着的。flatten: true} 选项会返回插槽的默许内容(若没有分发任何节点)。

看一下以下示例:

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

假定以上内容包括在一个叫做 <my-container> 的组件内部。

让我们检察一下该组件的差别用法,然后挪用 assignedNodes() 输出差别的效果:

第一例中,我们将往插槽中增加内容:

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

挪用 assignedNodes() 将会返回 [<span slot="slot1"> container text </span>]。注重效果为一个节点数组。

第二例中,将不增加内容:

<my-container> </my-container>

挪用 assignedNodes() 将会返回空数组 []

然则,假定增加 {flatten: true} 参数将会返回默许内容:[<p>Default content</p>]

同理,为了查找插槽中的元素,开发者能够挪用 assignedNodes() 来找出元素被挂载到哪一个组件插槽中。

事宜模子

Shadow DOM 中的事宜冒泡的经由是值得注重的。

事宜目的被调解为保护 Shadow DOM 的封闭性。当事宜被从新定位,看起来是由组件本身发生而不是组件的 Shadow DOM 内部元素。

这里有流传出 Shadow DOM 的事宜列表(另有一些只能在 Shadow DOM 内流传):

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

自定义事宜

默许状况下,自定义事宜不会流传出 Shadow DOM。开发者若想要分配自定义事宜且想要流传出 Shadow DOM,须要增加 bubbles: truecomposed: true 选项参数。

让我们瞧瞧相似如许的事宜分配:

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

浏览器兼容状况

能够经由过程搜检 attachShadow 来搜检是不是支撑 Shadow DOM 功用:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

《Shadow DOM 内部组织及怎样构建自力组件》

参考资料:

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