欢迎关注微信公众号:前端阅读室
web components的一个重要方面就是封装,这样才能保持标签结构、样式、行为和页面中的其它代码隔离。Shadow DOM API就是封装很关键的一部分,它为元素提供了隐藏、隔离DOM的方式。
注:Shadow DOM被Firefox >=63, Chrome, Opera, and Safari,Edge >=75支持。
概览
Shadow DOM允许你在常规的DOM树中隐藏Shadow DOM,Shadow DOM的根节点是shadow root,shadow root里可以附加任何DOM元素,和常规DOM没有区别。
下面我们介绍几个shadow DOM的术语:
- Shadow host(宿主):附加了shadow DOM的常规DOM
- Shadow tree:shadow DOM里面的DOM树
- Shadow boundary(边界):shadow DOM结束,常规DOM开始的位置
- Shadow root:Shadow tree的根节点
你可以像操作常规DOM一样操作shadow DOM里面的DOM。比如添加子节点、设置属性、样式,或者在shadow DOM添加<style>标签为整个shadow DOM设置样式。shadow DOM的不同之处在于,shadow DOM里面所有的代码操作都不会影响外面,所以对封装很有帮助。
shadow DOM其实不是新东西,在浏览器中其实已经使用很长时间了,浏览器使用它来封装内置DOM元素的结构,比如<video>元素,在它的shadow DOM里面就包含了一系列的button以及其一些其他控件。shadow DOM规范的制定可以让你封装并操作你自定义的元素。
基本使用
你可以使用Element.attachShadow()方法为任何元素附加shadow root。它的参数是一个对象,这个对象只有一个设置mode,值为open或closed。
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open意味着你可以在页面中使用JavaScript访问shadow DOM,比如使用Element.shadowRoot属性。
let myShadowDom = myCustomElem.shadowRoot;
如果mode=closed,你是无法在shadow DOM外面获取到shadow root的,myCustomElem.shadowRoot会返回null。比如,内置的<video>元素,它的设置就是mode=closed,所以video.shadowRoot返回的就是null。
如果你想让shadow DOM封装的自定义元素成为应用的一部分,可以自由访问,你需要设置mode=open。
let shadow = this.attachShadow({mode: 'open'});
操作shadow DOM和操作常规DOM是相似的,你可以使用相同的DOM API。
let para = document.createElement('p');
shadow.appendChild(para);
// etc.
简单例子
现在我们通过一个简单例子来演示一下如果使用shadow DOM来封装一个自定义元素——<popup-info>。这个元素有一张图片和一段文本,当focus在上面的时候,它会提示一些信息。
我们在JS文件中定义一个叫PopUpInfo的class,它继承了HTMLElement。
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// write element functionality in here
}
}
然后,创建shadow root
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
let shadow = this.attachShadow({mode: 'open'});
}
}
创建shadow DOM的结构
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
let shadow = this.attachShadow({ mode: "open" });
// Create spans
let wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
let icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
let info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
let text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
}
let img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// write element functionality in here
}
}
接下来,我们创建一个<style>标签为shadow DOM设置一些样式
// Create some CSS to apply to the shadow dom
let style = document.createElement('style');
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}`;
将shadow DOM添加到shadow root中
// attach the created elements to the shadow dom
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
我们使用customElements.define定义这个新元素。
// Define the new element
customElements.define('popup-info', PopUpInfo);
最后就可以在页面中使用它了
<popup-info
img="img/alt.png"
data-text="Your card validation code (CVC) is an extra
security feature — it is the last 3 or 4
numbers on the back of your card."
>
内部样式vs外部样式
在例子中我们使用<style/>给Shadow DOM设置样式,不过更好的方式可能是使用<link>引入外部样式。
// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Attach the created element to the shadow dom
shadow.appendChild(linkElem);
注:由于link的样式需要加载,所以可能会出现FOUC(flash of unstyled content )。
许多现代浏览器对<style>做了优化。如果是相同的节点,<style>可以克隆一份。如果是相同的内容,可以复用相同的样式。由于这些优化外部样式和内部样式的性能已经很接近了。
本文翻译自
欢迎关注微信公众号:前端阅读室