开发者使用Vue、React等框架来使用及创建定制的组件,Web组件是浏览器原生支持的替代这些框架的特性,主要涉及相对比较新的三个Web标准。这些Web标准允许JS使用新标签扩展HTML,扩展后的标签就是自成一体的、可重用的UI组件。
1 HTML模版
DocumentFragment是一种Node类型,可以临时充当一组同辈节点的父节点,方便将这些同辈节点作为一个单元来使用。可以使用document.createDocumentFragment()来创建DocumentFragment节点。创建DocumentFragment节点后,可以像使用Element一样。
DocumentFragment与Element的区别是在于它没有父节点,当向文档中插入DocumentFragment节点时,DocumentFragment本身并不会被插入,实际上插入的是它的子节点。
1.1 <template>标签
<template>标签及其子元素永远不会被浏览器渲染,其对应的是一个HTMLTemplateElement对象,这个对象只定义了一个content属性,该属性值是包含<template>所以子节点的DocumentFragment。
可以深度克隆这个DocumentFragment,然后把克隆的副本插入文档中需要的地方。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<template id="tep"><div class="grid"></div>
</template>
<div class="block"><span>hello block</span>
</div>
</body>
<script>let tep = document.querySelector("#tep");let block = document.querySelector(".block");block.append(tep.content.cloneNode(true));//深度克隆block.append(tep.content.cloneNode(true));
</script>
</html>
<style>.grid{width: 50px;height: 50px;background: blue;margin: 10px;}
</style>
2 自定义元素
customElement.define()方法用来自定义元素。第一个参数是标签名(必须包含一个连字符),第二个参数是HTMLElement的子类。
浏览器会自动调用自定义元素类的特定“生命期方法”,当自定义元素被插入文档时,会调用connectedCallback()方法;当自定义元素从文档中被移除时会调用disconnectedCallback()方法。
如果自定义元素定义了静态的observedAttributes属性,其值为一个属性名的数组,且如果任何这些命名属性在这个自定义元素的一个实例上被设置(或修改),浏览器会调用attributeChangeCallback()方法。这个回调可以根据属性值的变化采取必要的步骤以更新组件。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<button onclick="createCus()">添加元素</button>
<button onclick="removeCus()">移除元素</button>
<button onclick="addSize()">增加尺寸</button>
<button onclick="changeColor()">修改颜色属性</button>
<div id="block"></div>
</body>
</html><script>class CustomCircle extends HTMLElement {connectedCallback() {this.style.borderRadius = "50%";this.style.border = "solid black 1px"this.style.display = "block"this.style.width = "50px";this.style.height = "50px";this.classList.add("custom")this.customSize = 50;console.log("元素被添加到文档");}disconnectedCallback() {console.log("元素被移除");}static get observedAttributes() {return ["cus"];}get size() {return this.customSize;}set size(val) {this.customSize = val;this.style.width = val + "px";this.style.height = val + "px";}attributeChangedCallback(name,oldVal,newVal) {this.style.background = newVal;console.log(name,oldVal,newVal);}}customElements.define("custom-circle",CustomCircle);function createCus() {let cus = new CustomCircle();document.querySelector("#block").append(cus);}function removeCus() {let cus = document.querySelector(".custom")if (cus) cus.remove();}function addSize() {let cus = document.querySelector(".custom")if (cus) {cus.size = cus.size + 10;}}function changeColor() {let cus = document.querySelector(".custom")cus.setAttribute("cus","yellow");}</script>
2.1 组件渲染过程
浏览器在解析HTML文档时,当在Web组件还没有定义就遇到其标签时,浏览器会向DOM树中添加一个通用的HTMLElement,即便它们不知道要对它做什么。之后,当自定义元素有定义之后,这个通用元素会被“升级”,从而具备预期的外观与行为。
如果Web组件包含子元素,那么在组件有定义之前它们可能会被不适当地显示出来。可以使用下面的CSS将Web组件隐藏到它们有定义为止。(:not(:defined))。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<cus-ele><span>hello</span>
</cus-ele>
<cus-ele class="cus"><span>hello</span>
</cus-ele>
</body>
</html><style>
.cus:not(:defined) {opacity: 0;
}
</style>
3 影子DOM
Shadom DOM为封装而生,它可以让一个组件拥有自己的“影子”DOM树,这个DOM树不能在主文档中被任意访问,可能拥有局部样式规则,还有其他特性。
3.1 在浏览器中查看组件的影子DOM
浏览器在内部使用DOM/CSS来绘制影子DOM,这个DOM结构一般对用户是隐藏的,但我们可以开发者工具中看见它。在Chrome中,需要打开“Show user agent shadow DOM”选项。
图 <video>标签的影子DOM
我们不能使用一般的JS调用或者选择器来获取shadow DOM 元素,它们不是常规的子元素,而是一种封装手段。
3.2 为元素添加影子DOM
影子DOM允许把一个“影子根节点”附加给一个常规的HTML元素(或自定义元素),后者被称为“影子宿主”(shadow host)。
attachShadow方法是为常规元素附加一个影子根节点,其参数为一个ShadowRootInit类型的对象,其返回一个影子根节点。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div class="block"></div>
</body>
</html><script>class CustomEle extends HTMLElement {connectedCallback() {this.style.display = "block";this.style.width = "100px";this.style.height = "100px";this.style.border = "solid black 1px";let shadow = this.attachShadow({mode: 'closed'})shadow.innerHTML = "<div>hello shadow</div>"}}customElements.define("custom-ele",CustomEle);let cus = new CustomEle();document.querySelector(".block").append(cus);console.log(cus.shadowRoot); //如果mode 为open则有值,否则为空
</script>
3.2.1 影子DOM封装
1,在创建影子根节点并将其附加于影子宿主时,可以指定其模式开放还是关闭,关闭的影子根节点将被完全封闭,不可访问。如果开放,意味着影子宿主会有一个shadowRoot属性,js可以通过这个属性来访问影子根节点元素。
2,影子根节点下定义的样式不会影响外部的阳光DOM元素,类似地,影子宿主元素的阳光DOM样式也不会影响影子根节点。
3,影子DOM中发生的某些事件(如”load”)会被封闭在影子DOM中,另一些事件,如focus、mouse和键盘事件则会向上冒泡,穿透影子DOM。当一个发源于影子DOM内的事件跨过边界向阳光DOM传播时,其target属性会变成影子宿主元素。