目录
引言
概念
基本用法
attachShadow函数
mode(模式)
delegatesFocus(委托聚焦)
Custom Elements+Shadow DOM
基本用法
样式及属性隔离
写在最后
相关代码
参考文章
引言
上篇文章的自定义标签中,我们使用customElements对象对原生标签进行拓展,达到组件的拓展性与复用性的效果,那么如何保证组件的属性、结构及样式的封装隔离便是本篇文章将要分享的内容,本篇文章不仅仅会介绍Shadow DOM的基本用法,还会对前面说到的Custom Elements做一个使用场景的拓展
概念
在JS作用域一文中,我们提到全局作用域和局部作用域的概念,如果全局作用域没有处理好可能会导致作用域污染,出现问题。如果单纯的使用Custom Elements实现自定义组件的功能可能会存在样式冲突,Dom干扰,组件中的类名或者ID冲突,复用性复杂等问题,这会导致组件的可靠性和可维护性降低,此时一个独立的Dom容器就显得非常重要了。
影子Dom(Shadow DOM),可以理解为DOM中的DOM,它是前端组件的核心之一,它提供了一套将标签与DOM树隔离的机制,大大提升了自定义标签或者普通标签的封装性,可重用性和可维护性。
在使用Shadow DOM前,我们先了解一些专有名词
Shadow Host(影子宿主):Shadow DOM的容器。它是普通DOM中的一个元素,可以称为宿主元素。宿主元素可以是自定义的Web组件,例如一个自定义的标签、视频标签或任何其他的自定义元素。
Shadow Tree(影子的树):Shadow DOM内部的DOM树。在Shadow Host内部,开发者可以定义一个独立的DOM子树,其中包含组件的样式和结构,这个内部DOM树就是Shadow Tree。
Shadow Root(影子的根节点):Shadow Tree的根节点。通过使用Shadow Root,我们可以将Shadow Tree连接到Shadow Host上。影子根是Shadow DOM的入口点,通过Shadow Root我们可以访问Shadow Tree中的内容。
Shadow Boundary(影子的边界):Shadow DOM的隔离边界。Shadow DOM结束的地方,也是常规DOM开始的地方。这个边界将Shadow DOM和外部DOM分隔开来,防止它们互相干扰。外部DOM无法直接访问Shadow Tree的内容,只能访问到Shadow Host。
基本用法
使用element.attachShadow函数在宿主(Host)标签上创建根(Root)元素
<body><div id="host"></div><script>const hostEle = document.querySelector("#host")const treeEle = document.createElement("span")// 影子的树或后代节点treeEle.textContent = "treeEle"const rootEle = hostEle.attachShadow({ mode: 'open' });// 创建Shadow RootrootEle.appendChild(treeEle)// 将树添加到root标签</script>
</body>
attachShadow函数
attachShadow函数可以传入一个ShadowRootInit类型的参数,它有三个属性,分别是:mode,delegatesFocus,slotAssignment。
mode(模式)
mode是必传的值,代表是否允许外部访问和修改ShadowRoot的属性,open表示开放(允许),closed表示关闭(不允许)。
const hostEle = document.querySelector("#host")
const treeEle = document.createElement("span")
treeEle.textContent = "treeEle"
const rootEle = hostEle.attachShadow({ mode: 'closed' });// 不允许外部访问
rootEle.appendChild(treeEle)
console.log(hostEle.shadowRoot);// null
delegatesFocus(委托聚焦)
delegatesFocus可选,传入布尔值,表示是否减轻自定义元素的聚焦性能问题。当我们定义自定义标签时,标签可能不支持聚焦,此时让第一个可聚焦的部分成为焦点, 并且shadow host将提供所有可用的 :focus样式。怎么理解这句话?参考了许多网上的文章,做个总结,思考下面的代码:
<body><div id="host"></div><script>const hostEle = document.querySelector("#host")// 宿主元素const rootEle = hostEle.attachShadow({ mode: 'open', delegatesFocus: true });// 根元素const styleEle = document.createElement("style")// 样式元素styleEle.textContent = `div {width: 500px;height: 80px;border: 1px solid #000;}input:focus {background: lightblue;}`// 内部的其他树标签const treeEle = `<div><input type="text" /></div>`// 在Shadow DOM中添加全部元素rootEle.appendChild(styleEle)rootEle.innerHTML += treeEle</script>
</body>
当delegatesFocus设置为true,并点击外部的div时,内部的输入框会被聚焦,输入框是标签中可以被聚焦的元素;
当delegatesFocus设置为false,点击外部div就不会聚焦任何标签。
Custom Elements+Shadow DOM
基本用法
基于自定义标签和上面讲到的知识点,我们尝试将二者结合,实现一个简易的自定义组件,思考下面的代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ShadowDOM</title>
</head><body><my-custom-element></my-custom-element><script>const elemName = "my-custom-element"const ele = document.querySelector(elemName)class MyCustomElement extends HTMLElement {constructor() {super();this.initShadow()}// 基于当前自定义标签创建initShadow() {this.appendChild(this.attachShadow({mode: "open"}))this.shadowRoot.innerHTML = "<div>hello world</div>"}}customElements.define(elemName, MyCustomElement)</script>
</body></html>
样式及属性隔离
思考以下代码,通过props我们可以将组件的行为及数据传入自定义标签中,通过attachShadow隔离样式,自定义标签隔离属性和行为,达到一个组件容器的效果,其中this.shadowRoot就是标签的影子根元素,即this.attachShadow的产物
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ShadowDOM</title><style>div {/* 全局样式无法影响内部标签 */border: 1px solid black;}</style>
</head><body><my-custom-element></my-custom-element><script>const elemName = "my-custom-element"const ele = document.querySelector(elemName)class MyCustomElement extends HTMLElement {styleContent = ""// 组件局部样式treeEle = ""// 影子(树)子节点constructor() {super();this.initProps()this.initShadow()}// 通过props传入参数initProps() {const { ele, style } = this.propsthis.styleContent = stylethis.treeEle = ele}// 基于当前自定义标签创建initShadow() {this.appendChild(this.attachShadow({mode: "open"}))const styleEle = document.createElement("style")styleEle.textContent = this.styleContentthis.shadowRoot.appendChild(styleEle)this.shadowRoot.innerHTML += this.treeEle}}// 自定义标签参数ele.props = {ele: "<div>hello world</div>",style: `div {width: 100px;height: 100px;background: lightcoral;}`}customElements.define(elemName, MyCustomElement)console.log(document.querySelectorAll("div")); // [] 全局无法获取shadow中的标签console.log(ele.shadowRoot.querySelectorAll("div")[0].textContent);// hello world 可以通过element获取shadow标签</script>
</body></html>
上述代码可以看到,全局样式与元素只在当前的shadowDOM中可以访问,与全局作用域形成了隔离关系
写在最后
本文结合Custom Elements和Shadow DOM实现了一个简易的自定义组件,通过使用props传递参数,将组件的行为及数据传入自定义标签中,并通过attachShadow隔离样式,实现了属性、结构和样式的封装隔离,从而达到组件容器的效果。那么自定义组件之间如何进行通信?组件样式及选择器如何使用?在后续的系列文章中,我会对这些知识点做个详细说明,敬请期待!
感谢你看到了最后,如果觉得文章还不错的话,还请给个三连,感谢!
相关代码
myCode: 基于js的一些小案例或者项目 - Gitee.com
参考文章
影子 DOM(Shadow DOM)
使用 shadow DOM - Web API 接口参考 | MDN