JS的工作方式:Shadow DOM技术和Web组件


今天,在翻译了17篇专门介绍与JavaScript相关联的所有功能的材料时,我们将讨论Web组件和旨在与之合作的各种标准。 特别要注意Shadow DOM技术。



复习


Web组件是旨在描述适用于重用的新DOM元素的API系列。 这些元素的功能与其余代码分开;它们可以在我们自己设计的Web应用程序中使用。

与Web组件相关的技术有四种:

  • 影子DOM(影子DOM)
  • HTML模板(HTML模板)
  • 自定义元素
  • HTML导入(HTML导入)

在本文中,我们将讨论Shadow DOM技术,该技术旨在创建基于组件的应用程序。 它提供了解决您可能已经遇到的常见Web开发问题的方法:

  • DOM隔离:组件具有隔离的DOM树(这意味着document.querySelector()命令将不允许访问组件的影子DOM中的节点)。 另外,由于DOM组件是隔离的,因此它简化了Web应用程序中的CSS选择器系统,这使开发人员可以在不同的组件中使用相同的通用标识符和类名,而不必担心可能发生名称冲突。
  • CSS隔离:在影子DOM中描述的CSS规则仅限于此。 这些样式不会离开元素,也不会与其他页面样式混合。
  • 组成:为基于标记的组件开发声明性API。

Shadow DOM技术


假定您已经熟悉DOM和相关API的概念。 如果不是这样,您可以阅读材料。

Shadow DOM与常规DOM基本相同,但有两个区别:

  • 首先是Shadow DOM的创建和使用方式,尤其是有关Shadow DOM与页面其余部分的关系。
  • 第二个是Shadow DOM与页面有关的行为。

使用DOM时,将创建DOM节点,这些节点作为子级连接到页面的其他元素。 在Shadow DOM技术的情况下,创建了一个隔离的DOM树,该树连接了该元素,但与它的普通子元素分离。

这个孤立的子树称为影子树。 这样的树所附加的元素称为影子主机。 事实证明,添加到影子DOM子树的所有内容都是其所附着元素的本地元素,包括使用<style>标记描述的<style> 。 这就是通过Shadow DOM技术提供CSS隔离的方式。

创建一个影子DOM


影子根是附加到宿主元素的文档的一部分。 将影子根元素附加到该元素后,该元素将获取影子DOM。 为了为某个元素创建影子DOM,您需要使用element.attachShadow()形式的命令:

 var header = document.createElement('header'); var shadowRoot = header.attachShadow({mode: 'open'}); shadowRoot.appendChild(document.createElement('<p> Shadow DOM </p>'); 

应该注意的是, Shadow DOM 规范中 ,列出了DOM影子子树无法连接的元素列表。

Shadow DOM中的合成


组合是Shadow DOM的最重要功能之一,它是创建Web应用程序的一种方法,该方法用于编写HTML代码。 在此过程中,程序员将组成页面的各种构建块(元素)组合在一起,并在必要时将它们相互嵌套。 例如,这些元素是诸如<div><header><form>类的元素,以及用于创建Web应用程序界面的其他元素,包括那些充当其他元素的容器的元素。

组合确定元素的功能,例如<select><form><video> ,以包括其他HTML元素作为子元素,以及组织由不同元素组成的此类结构的特殊行为的能力。

例如, <select>元素具有用于以下拉列表的形式呈现<option>元素的装置,该下拉列表具有该列表的元素的预定内容。

考虑组成元素时使用的Shadow DOM的某些功能。

轻度dom


轻型DOM是由组件用户创建的标记。 此DOM在组件的影子DOM之外,并且是组件的子代。 想象一下,您创建了一个名为<better-button>的自定义组件,该组件扩展了标准HTML <button>元素的功能,并且用户需要向该新元素添加图像和一些文本。 看起来是这样的:

 <extended-button> <!--  img  span -  Light DOM  extended-button --> <img align="center" src="boot.png" slot="image"> <span>Launch</span> </extended-button> 

<extended-button>元素是程序员自己描述的自定义组件,该组件内的HTML代码是其Light DOM-该组件的用户向其添加的内容。

本示例中的影子DOM是<extended-button>组件。 这是组件的本地对象模型,它描述了与CSS外部世界隔离的内部结构,并封装了组件的实现细节。

扁平化的dom


Flattened DOM树表示浏览器如何将Light DOM和Shadow DOM结合在一起在屏幕上显示组件。 在开发人员工具中可以看到的就是这样的DOM树,它就是在页面上显示的。 它可能看起来像这样:

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

模式


如果必须在网页的HTML标记中不断使用相同的结构,则使用某个模板而不是一次又一次地编写相同的代码将很有用。 以前可以做到这一点,但是现在由于HTML <template>的出现,大大简化了一切,该<template>对现代浏览器提供了出色的支持。 该元素及其内容未显示在DOM中,但是您可以从JavaScript中使用它。 考虑一个简单的例子:

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

如果将此设计包含在页面的HTML标记中,则它所描述的<p>标记的内容在明确地附加到文档的DOM之前不会出现在屏幕上。 例如,它可能看起来像这样:

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

还有其他方法可以达到相同的效果,但是,正如已经提到的,模板是非常方便的标准工具,享有良好的浏览器支持。


HTML浏览器对现代浏览器的支持

模板本身是有用的,但与自定义元素一起使用时,其功能已完全公开。 自定义元素是单独材料的主题,现在,要了解正在发生的事情,就足以考虑到customElement浏览器customElement允许程序员描述自己的HTML标签并指定使用这些标签创建的元素在屏幕上的外观。

定义一个使用我们的模板作为其影子DOM内容的Web组件。 将此新元素称为<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()方法制作的模板内容的克隆附加到影子根。

由于我们将模板的内容附加到影子DOM,因此可以在<style>元素的模板中包含一些样式信息,然后将其封装在user元素中。 如果您使用常规DOM而不是Shadow 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模板有几个缺点,主要的缺点是模板包含静态标记,例如,不允许使用其帮助显示某些变量的内容,以使其与使用标准HTML的方式相同。模式。 这是<slot>标记所在的<slot>

插槽可以看作是占位符,可让您在模板中包含自己的HTML代码。 这使您可以创建通用HTML模板,然后通过向其添加插槽来使它们可自定义。

使用<slot>查看以上模板的外观:

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

如果在标记中包含元素时未指定插槽的内容,或者浏览器不支持使用插槽,则<my-paragraph>元素将仅包含Default text的标准内容。

为了设置插槽的内容,您需要在<my-paragraph>元素中包含带有slot属性的HTML代码,该属性的值等效于应放置此代码的插槽的名称。

和以前一样,可以有任何东西。 例如:

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

可以放置在插槽中的元素称为Slotable元素。

请注意,在前面的示例中,我们在插槽中添加了<span>元素,这就是所谓的sloted元素。 它具有为slot属性分配的值my-text ,即与模板中描述的插槽的name属性中使用的值相同的值。

处理完上述标记后,浏览器将创建以下Flattened 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-root 。 这只是影子DOM存在的一个指标。

程式化


使用Shadow DOM技术的组件可以在通用的基础上设置样式,它们可以定义自己的样式,或者以自定义CSS属性的形式提供挂钩,以允许组件用户覆盖默认样式。

components组件中描述的样式


CSS隔离是Shadow DOM技术最显着的功能之一。 即,我们正在谈论以下内容:

  • 放置了相应组件的页面的CSS选择器不会影响其内部内容。
  • 组件中描述的样式不会影响页面。 它们被隔离在宿主元素中。

影子DOM中使用的CSS选择器在本地应用于组件内容。 实际上,这意味着可以在不同的组件中重用相同的标识符和类名,而无需担心名称冲突。 简单的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允许您访问包含影子DOM树的元素并设置此元素的样式:

 <style> :host {   display: block; /*       display: inline */ } </style> 

使用:host:host ,请记住,父页面的规则比使用该伪类的元素中指定的规则具有更高的优先级。 这使用户可以从外部覆盖其中定义的主机组件样式。 另外, :host:host仅在影子根元素的上下文中起作用;您不能在影子DOM树之外使用它。

伪类的功能形式: :host(<selector>) ,如果与指定的<selector>元素匹配,则可以访问host元素。 这是允许组件封装响应用户操作或组件状态更改的行为的好方法,并允许您根据主机组件设置内部节点的样式:

 <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; /*     #tabs   -  class="pink". */ } </style> 

with带有伪类的主题和元素:host-context(<selector>)


如果:host-context(<selector>):host-context(<selector>)与host元素或其任何祖先与指定的<selector>元素匹配,则与host元素匹配。

此功能的常见用例是使用主题设置元素样式。 例如,通常通过将适当的类分配给<html><body>标签来使用主题:

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

如果此元素是.lightteme的后代, .lightteme:host-context(.lightheme):host-context(.lightheme)将应用于<fancy-tabs>

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

:host-context()构造对于应用主题可能有用,但是为此目的,最好使用带有自定义CSS属性的钩子。

from从外部造型组件的主体元素


可以使用其标签名称作为选择器在外部设置组件的宿主元素的样式:

 custom-container { color: red; } 

外部样式优先于影子DOM中定义的样式。
假设用户创建以下选择器:

 custom-container { width: 500px; } 

它将覆盖组件本身中定义的规则:

 :host { width: 300px; } 

使用这种方法,您只能样式化组件本身。 如何风格化组件的内部结构? 自定义CSS属性用于此目的。

using使用自定义CSS属性创建样式挂钩


如果组件的作者使用自定义CSS属性为它们提供样式挂钩,则用户可以自定义组件内部结构的样式。

这种方法基于一种类似于使用<slot>标记时使用的机制的机制,但是在这种情况下,它适用于样式。

考虑一个例子:

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

这是影子DOM树中的内容:

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

在这种情况下,组件使用黑色作为背景色,因为是由用户指定的。 否则,背景色将为#CECECE

作为组件的作者,您有责任告诉其用户可以使用哪些特定的CSS属性。 考虑组件的开放接口的这一部分。

用于插槽的JavaScript API


Shadow DOM API提供了使用插槽的功能。

▍活动时间变更


当放置在插槽中的节点发生更改时,将引发slotchange事件。 例如,如果用户在Light DOM中添加或删除子节点:

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

要跟踪Light DOM中的其他类型的更改,可以在元素的构造函数中使用MutationObserver在此处阅读有关此内容的更多信息。

▍方法AssignedNodes()


如果您需要知道哪些元素与该插槽关联,则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()调用将返回[ container text ] 。 请注意,此值是节点数组。

在第二种情况下,我们不使用自己的内容填充广告位:

 <my-container> </my-container> 

assignedNodes()调用将返回一个空数组- []

但是,如果将{flatten: true}参数传递给此方法,则对同一元素调用它会返回其默认内容: [ Default content ]

[ Default content ]

[ Default content ]

另外,为了访问插槽中的元素,可以调用assignedNodes()以让您知道元素分配给哪个组件插槽。

事件模型


我们来谈谈在影子DOM树中弹出的事件弹出时会发生什么。 设置事件的目的时要考虑到Shadow DOM技术支持的封装。 当事件被重定向时,它看起来好像来自组件本身,而不是来自其内部元素,该内部元素位于影子DOM树中,并且是该组件的一部分。

这是从DOM阴影树传递的事件列表(此行为不是某些事件的特征):

  • 焦点事件: blurfocusfocusinfocusout
  • 鼠标事件: clickdblclickmousedownmouseentermousemove等。
  • 车轮事件: wheel
  • 输入事件: beforeinputinput
  • 键盘事件: keydownkeyup
  • 合成事件: compositionstartcompositionupdatecompositionend
  • 拖动事件: dragstartdragdragenddrop等等。

自定义事件


默认情况下,用户事件不会离开DOM阴影树。 如果要触发事件,并希望它离开Shadow DOM,则需要为其提供参数bubbles: truecomposed: true 。 这是此类事件的调用的外观:

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

支持Shadow DOM浏览器


为了确定浏览器是否支持Shadow DOM技术,您可以检查attachShadow的存在:

 const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow; 

这是有关各种浏览器如何支持该技术的信息。


在浏览器中支持Shadow DOM技术

总结


影子DOM树的行为不像常规DOM树。 特别地,根据该材料的作者,在SessionStack库中,这是通过跟踪DOM更改的过程的复杂性来表达的,该信息需要有关哪个信息的信息才能再现页面所发生的情况。 即, MutationObserver用于跟踪更改。 在这种情况下,DOM影子树不会在全局范围内引发MutationObserver事件,这导致需要使用特殊方法来处理使用Shadow DOM的组件。

, - Shadow DOM, , , , .

亲爱的读者们! -, Shadow DOM?

Source: https://habr.com/ru/post/zh-CN415881/


All Articles