开发者使用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属性会变成影子宿主元素。