内容简介:Shadow DOM 是 Web Components 定义的四大标准之一。Shadow DOM 解决了构建网络应用的脆弱性问题。脆弱性是由 HTML、CSS 和 JS 的全局性引起的。Shadow DOM 为网络开发中的常见问题提供解决方案:(图片来源 —— MDN Shadow DOM)
Shadow DOM 是 Web Components 定义的四大标准之一。Shadow DOM 解决了构建网络应用的脆弱性问题。脆弱性是由 HTML、CSS 和 JS 的全局性引起的。
Shadow DOM 为网络开发中的常见问题提供解决方案:
-
隔离 DOM:组件的 DOM 是独立的(例如,
document.querySelector()
不会返回组件 Shadow DOM 中的节点)。 -
作用域 CSS:Shadow DOM 内部定义的 CSS 在其作用域内。样式规则不会泄漏,页面样式也不会渗入。
-
组合:为组件设计一个声明性、基于标记的 API。
-
简化 CSS: 作用域 DOM 意味着您可以使用简单的 CSS 选择器,更通用的 id/class 名称,而无需担心命名冲突。
-
效率:将应用看成是多个 DOM 块,而不是一个大的(全局性)页面。
(图片来源 —— MDN Shadow DOM)
Shadow DOM vs DOM
HTML 因其易于使用的特点驱动着网络的发展。通过声明几个标记,即可在几秒内编写一个带有图文信息和结构的页面。 但是,HTML 自身的功能并不强大。 对于我们人类而言,理解基于文本语言很容易,但是机器需要更多帮助才能理解。 因此,文档对象模型(DOM) 应运而生。
Shadow DOM 与普通 DOM 相同,但有两点区别:
1) 创建/使用的方式;
2) 与页面其他部分有关的行为方式。
这里以 “创建/使用的方式” 为例:
创建 DOM
const header = document.createElement('header'); const h1 = document.createElement('h1'); h1.textContent = 'Hello world!'; header.appendChild(h1); document.body.appendChild(header);
创建 Shadow DOM
const header = document.createElement('header'); const shadowRoot = header.attachShadow({mode: 'open'}); shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';
需要注意的是,并不是所有的元素都可以挂载 Shadow DOM,其主要原因是:
- 浏览器已为该元素托管其自身的内部 shadow DOM(比如
textarea
、input
)。 - 让元素托管 shadow DOM 毫无意义 (比如
img
)。
所以以下方法是行不通的:
document.createElement('input').attachShadow({mode: 'open'});
另外使用 Shadow DOM 时,有以下的注意事项:
-
Shadow DOM,一旦创建就无法删除,它只能用新的替换。
-
要查看浏览器如何为 input 或 textarea 等元素实现 shadow DOM,对 Chrome 用户来说可以按照:
DevTools > Settings > Preferences > Elements -> [x] Show user agent shadow DOM
路径启用对应的选项。
Shadow DOM vs Light DOM
Light DOM
组件用户编写的标记,该 DOM 不在组件 shadow DOM 之内,它是元素实际的子项。
<button is="better-button"> <img src="gear.svg" slot="icon"> <span>Settings</span> </button>
Shadow DOM
该 DOM 是由组件的作者编写。Shadow DOM 对于组件而言是本地的,它定义内部结构、作用域 CSS 并封装实现详情。它还可定义如何渲染由组件使用者编写的标记。
#shadow-root <style>...</style> <slot name="icon"></slot> <span id="wrapper"> <slot>Button</slot> </span>
如何创建 Shadow DOM
<div class="dom"></div> <script> let el = document.querySelector('.dom'); el.attachShadow({ mode: 'open' }); el.shadowRoot.innerHTML = 'Hi I am shadowed!'; // 创建新的元素 let hello = document.createElement('span'); hello.textContent = 'Hi I am shadowed but wrapped in span'; el.shadowRoot.appendChild(hello); </script>
什么是 Shadow Root
ShadowRoot 是 Shadow DOM 的根,从技术上讲,它是一个非元素节点,是一种特殊的文档片段。你可以通过 ShadowRoot 对象上的 appendChild
、 querySelectorAll
等方法去操作整个 Shadow DOM 树。
对于一个普通的元素,比如 <div>
,你可以通过调用该对象上的 attachShadow
方法来创建一个ShadowRoot, attachShadow
接受一个对象进行初始化,这个对象有一个 mode
属性,它有两个取值: 'open'
和 'closed'
,这个属性是在创造 ShadowRoot 的时候需要初始化提供的,并在创建 ShadowRoot 之后成为一个只读属性。
那么 mode: 'open'
和 mode: 'closed'
有什么区别呢?
let $element = document.createElement("div"); $element.attachShadow({ mode: "open" }); $element.shadowRoot
在调用 attachShadow
创建 ShadowRoot 之后, attachShdow
方法会返回 ShadowRoot 对象实例,你可以通过这个返回的对象去构造整个 Shadow DOM。
当 mode 为 'open'
时,在用于创建 ShadowRoot 的外部普通节点(比如 <div>
)上,会有一个 shadowRoot
属性,这个属性也就是创造出来的那个 ShadowRoot,也就是说,你可以通过这个属性获取 ShadowRoot,进而对它进行操作。
而当 mode 为 'closed'
时,你将不能再得到这个属性,这个属性会被设置为 null
,比如:
let $element = document.createElement("div"); $element.attachShadow({ mode: "closed" }); $element.shadowRoot // null
一般情况下总是使用 open
模式调用 attachShadow 方法,这样的话可以让组件作者和用户都可以根据需要进行相关操作。
如何设定样式
Shadow DOM 最有用的功能是 作用域 CSS:
- 外部页面中的 CSS 选择器不应用于组件内部。
- 内部定义的样式也不会渗出,它们的作用域仅限于宿主元素。
设置宿主元素样式
<style> :host { display: block; contain: content; } </style>
使用 :host
的一个问题是,父页面中的规则较之在元素中定义的 :host
规则具有更高的特异性。 也就是说,外部样式优先。这可让用户从外部替换你已定义的样式。 此外, :host
仅在影子根范围内起作用,因此无法在 shadow DOM 之外使用。
除了 :host
之外,还支持 :host(<selector>)
的函数形式,它可让你基于宿主将对用户互动或状态的反应行为进行封装,或对内部节点进行样式设定。
<style> :host { opacity: 0.4; will-change: opacity; transition: opacity 300ms ease-in-out; } :host(:hover) { opacity: 1; } :host([disabled]) { background: grey; pointer-events: none; opacity: 0.4; } :host(.blue) { color: blue; } :host(.pink) > #tabs { color: pink; </style>
基于情景设定样式
如果 :host-context(<selector>)
或其任意父级与 <selector>
匹配,它将与组件匹配。 一个常见用途是根据组件的环境进行主题化。 例如,很多人都通过将 class 应用到 <html>
或 <body>
进行主题化:
<body class="darktheme"> <fancy-tabs> ... </fancy-tabs> </body>
如果 :host-context(.darktheme)
为 .darktheme
的子级,它将对 <fancy-tabs>
进行样式化:
:host-context(.darktheme) { color: white; background: black; }
为分布式节点设定样式
比如说我们已创建了一个 name badge 组件:
<name-badge> <h2>Eric Bidelman</h2> <span class="title"> Digital Jedi, <span class="company">Google</span> </span> </name-badge>
组件的 shadow DOM 可为用户的 <h2>
和 .title
设定样式:
<style> ::slotted(h2) { margin: 0; font-weight: 300; color: red; } ::slotted(.title) { color: orange; } /* 以下方式不生效,因为::slotted()只支持顶层元素 ::slotted(.company), ::slotted(.title .company) { text-transform: uppercase; } */ </style> <slot></slot>
从外部为组件设定样式
有几种方法可从外部为组件设定样式:最简单的方法是使用标签名称作为选择器:
fancy-tabs { width: 500px; color: red; } fancy-tabs:hover { box-shadow: 0 3px 3px #ccc; }
外部样式总是优先于在 shadow DOM 中定义的样式。例如,如果用户编写选择器 fancy-tabs { width: 500px; }
,它将优先于组件的规则: :host { width: 650px;}
。
使用 CSS 自定义属性创建样式钩子
如果组件的作者通过 CSS 自定义属性 提供样式钩子,则用户可调整内部样式。 从概念上看,这与 <slot>
类似。 你创建 “样式占位符” 以便用户进行替换。
比如 <fancy-tabs>
可让用户替换背景颜色:
<!-- main page --> <style> fancy-tabs { margin-bottom: 32px; --fancy-tabs-bg: black; } </style> <fancy-tabs background>...</fancy-tabs>
在其 Shadow DOM 内部:
:host([background]) { background: var(--fancy-tabs-bg, #9E9E9E); border-radius: 10px; padding: 10px; }
在本例中,该组件将使用 black
作为背景值,因为用户指定了该值。 否则,背景颜色将采用默认值 #9E9E9E
。
浏览器支持
(图片来源 —— https://caniuse.com/#feat=shadowdomv1)
由上图可知 Chrome 53、Opera 40 和 Safari 10 以上的版本是支持 shadow DOM v1 标准。 Edge 也在考虑中,并具有 较高的优先级 。
你可以通过以下方法来检测当前浏览器是否支持 shadow DOM v1 标准:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
此外对于还不支持 shadow DOM v1 标准的浏览器来说,你也可以引入 shadydom 和 shadycss polyfill 来模拟 v1 的标准。Shady DOM 可以模拟 Shadow DOM 的 DOM 作用域,而 shadycss polyfill 则可以模拟原生 API 提供的 CSS 自定义属性和样式作用域。具体的使用方式,感兴趣的同学,请参考相应的开发文档,这里不再进一步说明。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。