这是专门探索 JavaScript 及其所构建的组件的系列文章的第 17 篇。html
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!前端
若是你错过了前面的章节,能够在这里找到它们:node
Web Components 是一套不一样的技术,容许你建立可重用的定制元素,它们的功能封装在你的代码以外,你能够在 Web 应用中使用它们。git
Web组件由四部分组成:github
在本文中主要讲解 Shadow DOM(影子DOM)web
Shadow DOM 这款工具旨在构建基于组件的应用。所以,可为网络开发中的常见问题提供解决方案:编程
本文假设你已经熟悉 DOM 及其它的 Api 的概念。若是不熟悉,能够在这里阅读关于它的详细文章—— https://developer.mozilla.org...。segmentfault
阴影 DOM 只是一个普通的 DOM,除了两个区别:数组
一般,你建立 DOM 节点并将其附加至其余元素做为子项。 借助于 shadow DOM,您能够建立做用域 DOM 树,该 DOM 树附加至该元素上,但与其自身真正的子项分离开来。这一做用域子树称为影子树。被附着的元素称为影子宿主。 您在影子中添加的任何项均将成为宿主元素的本地项,包括 <style>。 这就是 shadow DOM 实现 CSS 样式做用域的方式浏览器
一般,建立 DOM 节点并将它们做为子元素追加到另外一个元素中。借助于 shadow DOM,建立一个做用域 DOM 树,附该 DOM 树附加到元素上,但它与实际的子元素是分离的。这个做用域的子树称为 影子树,被附着的元素称为影子宿主。向影子树添加的任何内容都将成为宿主元素的本地元素,包括 <style>
,这就是 影子DOM 实现 CSS 样式做用域的方式。
影子根是附加到“宿主”元素的文档片断,元素经过附加影子根来获取其 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);
规范定义了元素列表,这些元素没法托管影子树,元素之因此在所选之列,其缘由以下:
<textarea>
、<input>
)。例如,如下方法行不通:
document.createElement('input').attachShadow({mode: 'open'}); // Error. `<input>` cannot host shadow 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 和 封装实现细节。
浏览器将用户建立的 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>
若是须要 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 也有至关不错的浏览器支持:
模板自己是有用的,但它们与自定义元素配合会更好。 能够 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>
模板有一些缺点,主要是静态内容,它不容许咱们渲染变量/数据,好可让咱们按照通常使用的标准 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 最大的特性之一:
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
伪类选择器,用来选择组件宿主元素中的元素 (相对于组件模板内部的元素)。
<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>)
或其任意父级与 <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; }
组件样式一般只会做用于组件自身的 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 自定义属性提供样式钩子,则用户可调整内部样式。其思想相似于<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 定制属性,将其视为组件的公共接口的一部分。
Shadow DOM API 提供了使用 slot 和分布式节点的实用程序,这些实用程序在编写自定义元素时早晚派得上用场。
当 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 的内部结构以及如何使用它。
有时候,了解哪些元素与 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 传播出去的事件列表(有些没有):
默认状况下,自定义事件不会传播到 Shadow DOM 以外。若是但愿分派自定义事件并使其传播,则须要添加 bubbles: true
和 composed: true
选项。
让咱们看看派发这样的事件是什么样的:
var container = this.shadowRoot.querySelector('#container'); container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));
如但愿得到 shadow DOM 检测功能,请查看是否存在 attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
有史以来第一次,咱们拥有了实施适当 CSS 做用域、DOM 做用域的 API 原语,而且有真正意义上的组合。 与自定义元素等其余网络组件 API 组合后,shadow DOM 提供了一种编写真正封装组件的方法,无需花多大的功夫或使用如 <iframe> 等陈旧的东西。
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!