摘要:向影子树添加的任何内容都将成为宿主元素的本地元素,包括,这就是影子实现样式作用域的方式。
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 17 篇。
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!
如果你错过了前面的章节,可以在这里找到它们:
JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!
JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!
JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏!
JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!
JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景!
JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!
JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!
JavaScript 是如何工作的:Web 推送通知的机制!
JavaScript是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!
JavaScript是如何工作的:渲染引擎和优化其性能的技巧!
JavaScript是如何工作的:深入网络层 + 如何优化性能和安全!
JavaScript是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!
JavaScript的如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!
JavaScript是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换!
JavaScript是如何工作的:存储引擎+如何选择合适的存储API!
概述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 树附加至该元素上,但与其自身真正的子项分离开来。这一作用域子树称为影子树。被附着的元素称为影子宿主。 您在影子中添加的任何项均将成为宿主元素的本地项,包括
如果需要 Web 页面上重复使用相同的标签结构时,最好使用某种类型的模板,而不是一遍又一遍地重复相同的结构。这在以前也是可以实现,但是 HTML 元素(在现代浏览器中得到了很好的支持)使它变得容易得多。此元素及其内容不在 DOM 中渲染,但可以使用 JavaScript 引用它。
一个简单的例子:
Paragraph content.
这不会出现在页面中,直到使用 JavaScrip t引用它,然后使用如下方式将其追加到 DOM 中:
var template = document.getElementById("my-paragraph"); var templateContent = template.content; document.body.appendChild(templateContent);
到目前为止,已经有其他技术可以实现类似的行为,但是,正如前面提到的,将其原生封装起来是非常好的,Templates 也有相当不错的浏览器支持:
模板本身是有用的,但它们与自定义元素配合会更好。 可以 customElement Api 能定义一个自定义元素,并且告知 HTML 解析器如何正确地构造一个元素,以及在该元素的属性变化时执行相应的处理。
让我们定义一个 Web 组件名为
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 中,所以可以在模板中使用
Paragraph content.
现在自定义组件可以这样使用:
模板有一些缺点,主要是静态内容,它不允许我们渲染变量/数据,好可以让我们按照一般使用的标准 HTML 模板的习惯来编写代码。Slot 是组件内部的占位符,用户可以使用自己的标记来填充。让我们看看上面的模板怎么使用 slot :
Default text
如果在标记中包含元素时没有定义插槽的内容,或者浏览器不支持插槽,
为了定义插槽的内容,应该在
Let"s have some different text!
可以插入插槽的元素称为 Slotable; 当一个元素插入一个插槽时,它被称为开槽 (slotted)。
注意,在上面的例子中,插入了一个 元素,它是一个开槽元素,它有一个属性 slot,它等于 my-text,与模板中的 slot 定义中的 name 属性的值相同。
在浏览器中渲染后,上面的代码将构建以下扁平 DOM 树:
设定样式#shadow-root
Let"s have some different text!
使用 shadow DOM 的组件可通过主页来设定样式,定义其自己的样式或提供钩子(以 CSS 自定义属性的形式)让用户替换默认值。
组件定义的样式作用域 CSS 是 Shadow DOM 最大的特性之一:
外部页面的 CSS 选择器不应用于组件内部
组件内定义的样式不会影响页面的其他元素,它们的作用域是宿主元素
shadow DOM 内部使用的 CSS 选择器在本地应用于组件实际上,这意味着我们可以再次使用公共vid/类名,而不用担心页面上其他地方的冲突,最佳做法是在 Shadow DOM 内使用更简单的 CSS 选择器,它们在性能上也不错。
看看在 #shadow-root 定义了一些样式的:
#shadow-root
上面例子中的所有样式都是#shadow-root的本地样式。使用元素在#shadow-root中引入样式表,这些样式表也都属于本地的。
:host 伪类选择器使用 :host 伪类选择器,用来选择组件宿主元素中的元素 (相对于组件模板内部的元素)。
当涉及到 :host 选择器时,应该小心一件事:父页面中的规则具有比元素中定义的 :host 规则具有更高的优先级,这允许用户从外部覆盖顶级样式。而且 :host 只在影子根目录下工作,所以你不能在Shadow DOM 之外使用它。
如果 :host(
:host-context(
:host-context(
比如,很多人都通过将类应用到 或 进行主题化:
…
在下面的例子中,只有当某个祖先元素有 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 自定义属性提供样式钩子,则用户可调整内部样式。其思想类似于
看看下面的例子:
…
在其 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 的备用内容(前提是没有分布任何节点)。
让我们看看下面的例子:
Default content
假设这是在一个名为
看看这个组件的不同用法,以及调用 assignedNodes() 的结果是什么:
在第一种情况下,我们将向 slot 中添加我们自己的内容:
container text
调用 assignedNodes() 会得到 [ container text ],注意,结果是一个节点数组。
在第二种情况下,将内容置空:
调用 assignedNodes() 的结果将返回一个空数组 []。
在第三种情况下,调用 slot.assignedNodes({flatten: true}),得到结果是: [
默认内容
]。此外,要访问 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: 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 提供了一种编写真正封装组件的方法,无需花多大的功夫或使用如
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!
欢迎加入前端大家庭,里面会经常分享一些技术资源。文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/108898.html
摘要:此即如何实现局部样式化的原理。这是一个绝佳的方式,开发者可以在组件内部封装响应用户交互或者状态的行为,然后基于宿主元素来样式化内部节点。 原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 这是 JavaScript 工作原理的第十七章。 showImg(https://segmentfault.com/img/remote/1460000...
摘要:为了方便大家共同学习,整理了之前博客系列的文章,目前已整理是如何工作这个系列,可以请猛戳博客查看。以下列出该系列目录,欢迎点个星星,我将更友动力整理理优质的文章,一起学习。 为了方便大家共同学习,整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作这个系列,可以请猛戳GitHub博客查看。 以下列出该系列目录,欢迎点个星星,我将更友动力整理理优质的文章,一起学习。 J...
摘要:与大多数全局对象不同,没有构造函数。为什么要设计更加有用的返回值早期写法写法函数式操作早期写法写法可变参数形式的构造函数一般写法写法当然还有很多,大家可以自行到上查看什么是代理设计模式代理模式,为其他对象提供一种代理以控制对这个对象的访问。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 19 篇。 如果你错过了前面的章节,可以在这里找到它们: 想阅读更多优质文章请...
Create by jsliang on 2019-2-11 15:30:34 Recently revised in 2019-3-17 21:30:36 Hello 小伙伴们,如果觉得本文还不错,记得给个 star , 小伙伴们的 star 是我持续更新的动力!GitHub 地址 并不是只有特定的季节才能跑路,只因为人跑得多了,这条路就定下来了。 金三银四跳槽季,jsliang 于 2019...
摘要:挂机科了次使用这个结构,匿名函数就有了自己的执行环境或闭包,然后我们立即执行。注意,匿名函数的圆括号是必需的,因为以关键字开头的语句通常被认为是函数声明请记住,中不能使用未命名的函数声明。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 20 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们: ...
阅读 1154·2023-04-25 17:28
阅读 3530·2021-10-14 09:43
阅读 3953·2021-10-09 10:02
阅读 1941·2019-08-30 14:04
阅读 3127·2019-08-30 13:09
阅读 3268·2019-08-30 12:53
阅读 2895·2019-08-29 17:11
阅读 1821·2019-08-29 16:58