文章目录
web Components的概念和使用使用自定义元素自定义元素类型实现自定义元素注册自定义元素自定义元素的回调函数使用自定义元素添加自定义元素的属性变化自定义元素示例 使用影子Dom影子Dom的原理示意图创建一个影子Domjs隔离css隔离影子Dom和自定义元素补充: 影子根ShadowRoot的相关属性和方法 使用html模板template标签实现自定义元素slots插槽实现自定义元素补充:插槽slot身上的属性和方法Web Component即web组件,允许创建可重用的定制元素(它们的功能封装在你的代码之外)。就是
组件
的功能。
web Components的概念和使用
web Components 可以创建封装了指定功能的定制元素,创建完成之后你可以在任何你喜欢的地方重用。
webComponents的三大组成部分:
<template>
和 <slot>
元素可以作为标记模板 使用自定义元素
自定义元素即由 Web 开发人员自行定义 HTML 元素,扩展浏览器中可用的元素集。
自定义元素类型
有两种类型:
自定义内置元素:继承自标准的 HTML 元素
:例如 HTMLImageElement 或 HTMLParagraphElement
。它们的实现定义了标准元素的行为。无需从头开始实现行为。独立自定义元素:继承自 HTML 元素基类 HTMLElement
。必须从头开始实现它们的行为。 实现自定义元素
自定义元素作为一个类
来实现。
在类的构造函数中:
可以设置初始状态和默认值,注册事件监听器,创建一个影子根(shadow root);
不应检查元素的属性或子元素,也不应添加新的属性或子元素
class WordCount extends HTMLParagraphElement { constructor() { super(); } // 此处编写元素功能}
独立自定义元素实现格式 class PopupInfo extends HTMLElement { constructor() { super(); } // 此处编写元素功能}
注册自定义元素
Window.customElements.define(name,constructor,options)
方法。
单个属性 extends 的对象
,该属性表示要扩展的内置元素。 customElements.define("word-count", WordCount, { extends: "p" });customElements.define("popup-info", PopupInfo);
自定义元素的回调函数
自定义元素生命周期回调包括:
connectedCallback()
:每当元素添加到文档中时调用。规范建议开发人员尽可能在此回调中实现自定义元素的设定,而不是在构造函数中实现。disconnectedCallback()
:每当元素从文档中移除时调用。adoptedCallback()
:每当元素被移动到新文档中时调用。attributeChangedCallback()
:在属性更改、添加、移除或替换时调用。 配置方式,和构造函数同级:
// 为这个元素创建类class MyCustomElement extends HTMLElement { static observedAttributes = ["color", "size"]; constructor() { // 必须首先调用 super 方法 super(); } connectedCallback() { console.log("自定义元素添加至页面。"); }}// 注册自定义元素customElements.define("my-custom-element", MyCustomElement);
使用自定义元素
自定义内置元素的使用要使用自定义内置元素,请使用内置元素,但将自定义名称作为 is 属性的值:
<p is="word-count"></p>
独立自定义元素的使用使用独立自定义元素,就像使用内置的 HTML 元素一样,使用自定义名称即可:
<popup-info> <!-- 元素的内容 --></popup-info>
添加自定义元素的属性变化
元素的属性修改之后元素也应该响应相应的变化,所以我们需要实现自定义元素的属性变化。
实现自定义元素的属性变化需要在自定义类中添加如下两个内容:
observedAttributes
的静态属性,它是一个数组,里面包含的是标签需要变更通知的所有的属性名称。添加attributeChangedCallback()
生命周期回调函数,实现属性变更的相关操作。回调接受三个参数:发生变化的属性的名称、属性的旧值、属性的新值.
attributeChangedCallback() 在元素的首次声明解析时也会被调用。
eg:
// 为这个元素创建类class MyCustomElement extends HTMLElement { static observedAttributes = ["size"]; constructor() { super(); } attributeChangedCallback(name, oldValue, newValue) { console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}。`); }}customElements.define("my-custom-element", MyCustomElement);
自定义元素示例
定义独立自定义元素定义元素js代码
// 为当这个元素创建一个类class PopupInfo extends HTMLElement { constructor() { // 必须首先调用 super 方法 super(); } connectedCallback() { // 创建影子根 const shadow = this.attachShadow({ mode: "open" }); // 创建几个 span const wrapper = document.createElement("span"); wrapper.setAttribute("class", "wrapper"); const icon = document.createElement("span"); icon.setAttribute("class", "icon"); icon.setAttribute("tabindex", 0); const info = document.createElement("span"); info.setAttribute("class", "info"); // 获取属性内容然后将其放入 info 这个 span 内 const text = this.getAttribute("data-text"); info.textContent = text; // 插入图标 let imgUrl; if (this.hasAttribute("img")) { imgUrl = this.getAttribute("img"); } else { imgUrl = "img/default.png"; } const img = document.createElement("img"); img.src = imgUrl; icon.appendChild(img); // 创建一些 CSS 应用于影子 DOM const style = document.createElement("style"); console.log(style.isConnected); 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; top: 20px; left: 10px; z-index: 3; } img { width: 1.2rem; } .icon:hover + .info, .icon:focus + .info { opacity: 1; } `; // 将创建好的元素附加到影子 DOM 上 shadow.appendChild(style); console.log(style.isConnected); shadow.appendChild(wrapper); wrapper.appendChild(icon); wrapper.appendChild(info); }}// 注册自定义元素customElements.define("popup-info", PopupInfo);
在html中使用
<popup-info img="图片icon地址" data-text="悬浮显示的文字内容"></popup-info>
效果:
目标:扩展内置的
<ul>
元素,以支持展开和折叠列表项。定义js代码:
// 为这个元素创建类class ExpandingList extends HTMLUListElement { constructor() { // 必须首先调用 super 方法 // super() 的返回值是对当前元素的引用 self = super(); } connectedCallback() { // 获取当前自定义 ul 元素的 ul 和 li 子元素 // 包含 ul 的 li 元素可以成为容器 const uls = Array.from(self.querySelectorAll("ul")); const lis = Array.from(self.querySelectorAll("li")); // 隐藏所有子 ul // 当用户点击更高级别的容器时,这些列表就会显示出来 uls.forEach((ul) => { ul.style.display = "none"; }); // 仔细观察每个在 ul 中的 li 元素 lis.forEach((li) => { // 如果这个 li 有一个 ul 作为子元素,则对其进行装饰并添加一个点击处理程序 if (li.querySelectorAll("ul").length > 0) { // 添加一个属性,以便通过样式使用 // 来显示打开或关闭的图标 li.setAttribute("class", "closed"); // 将 li 元素的文本包裹在一个新的 span 元素中 // 这样我们就可以将样式和事件处理程序分配给 span const childText = li.childNodes[0]; const newSpan = document.createElement("span"); // 从 li 复制文本到 span,设置光标样式 newSpan.textContent = childText.textContent; newSpan.style.cursor = "pointer"; // 为这个 span 添加事件处理程序 newSpan.addEventListener("click", (e) => { // span 的下一个兄弟元素应该是 ul const nextul = e.target.nextElementSibling; // 切换可见状态并更新 ul 的 class 属性 if (nextul.style.display == "block") { nextul.style.display = "none"; nextul.parentNode.setAttribute("class", "closed"); } else { nextul.style.display = "block"; nextul.parentNode.setAttribute("class", "open"); } }); // 添加 span 并从 li 中移除纯文本节点 childText.parentNode.insertBefore(newSpan, childText); childText.parentNode.removeChild(childText); } }); }}//注册元素customElements.define("expanding-list", ExpandingList, { extends: "ul" });
html使用:
<ul is="expanding-list"> <li> 列表1 <ul is="expanding-list"> <li> 列表1的第一级子列表1-1 <ul is="expanding-list"> <li> 列表1的第二级子列表1-1-1 <ul is="expanding-list"> <li>列表1的第三级子列表1-1-1-1</li> <li>列表1的第三级子列表1-1-1-2</li> </ul> </li> <li>列表1的第二级子列表1-1-2</li> </ul> </li> <li> 列表1的第一级子列表1-2 <ul is="expanding-list"> <li>列表1的第二级子列表1-2-1</li> <li>列表1的第二级子列表1-2-2</li> </ul> </li> </ul> </li> <li>列表2</li> </ul>
效果:
使用影子Dom
影子DOM的作用:保护我们的自定义元素,防止因为在页面的js代码中修改自定义元素的实现而意外地破坏我们的自定义元素。
不设置影子Dom的时候我们自定义的元素就是赤裸裸的放在页面的Dom节点中,谁想修改就可以直接获取元素进行修改,这样很可能破坏我们的自定义元素。而使用影子Dom就可以将我们的自定义元素在页面的Dom中”隐藏“,从而达到保护的效果
影子Dom的原理示意图
影子 DOM 允许将隐藏的 DOM 树
附加到常规 DOM 树中的元素
上——这个影子 DOM 始于一个影子根,在其之下你可以用与普通 DOM 相同的方式附加任何元素。
其中隐藏的dom树是不能通过document.querySelectorAll
获取的,需要使用shadowRoot.querySelectorAll
获取,同时我们可以设置开关来控制是否能够通过shadowRoot.querySelectorAll
获取到元素(mode参数的设置),从而外界无法随意获取改变元素的效果。
如果我们将自定义的元素放置在隐藏的dom树中就可以实现保护的效果。我们需要通过影子Dom
将自定义的元素放置在隐藏的dom树。
几个概念:
影子宿主(Shadow host): 影子 DOM 附加到的常规 DOM 节点。影子树(Shadow tree): 影子 DOM 内部的 DOM 树。影子边界(Shadow boundary): 影子 DOM 终止,常规 DOM 开始的地方。影子根(Shadow root): 影子树的根节点。创建一个影子Dom
我们先不讨论自定义元素的情况,先理解普通元素在隐藏dom树的情况。
创建影子Dom的语法:影子宿主.attachShadow({ mode: "open" })
mode参数的作用:
当 mode 设置为 "open"
时,页面中的 JavaScript 可以通过影子宿主的 shadowRoot
属性访问影子 DOM 的内部。
当 mode 设置为 "closed"
时,页面中的 JavaScript 不能访问影子 DOM 的内部。
<body><div id="host"></div><span>I'm not in the shadow DOM</span></body><script>const host = document.querySelector("#host");const shadow = host.attachShadow({ mode: "open" });const span = document.createElement("span");span.textContent = "I'm in the shadow DOM";//添加为影子dom节点shadow.appendChild(span);</script>
js隔离
影子Dom和标准Dom使用js获取节点的方式是不一样的,即js获取隔离。
使用document
获取元素是不会获取到影子dom上的节点的。添加如下js代码
const spans = Array.from(document.querySelectorAll("span")); for (const span of spans) { span.textContent = span.textContent.toUpperCase(); }
效果是只有标准Dom中的span元素变成大写:
shadowRoot
可以获取到影子Dom上的节点语法:
影子宿主.shadowRoot.获取元素节点的方法()
添加如下js代码
const spans = Array.from(host.shadowRoot.querySelectorAll("span"));for (const span of spans) { span.textContent = span.textContent.toUpperCase(); }
效果是只有影子Dom中的span元素变成大写:
创建影子Dom的时候只需要设置
{mode: "closed"}
就可以,此时shadowRoot
返回null。 css隔离
页面中的style样式对影子Dom中的节点是不起作用的。
页面中设置如下样式:<style> span { color: rgb(173, 77, 77); border: 1px solid black; border-radius: 10px; background: black; padding: 2px 5px; } </style>
效果:
可以使用编程式或声明式的方法为影子Dom添加样式
编程式:通过构建一个CSSStyleSheet
对象并将其附加到影子根。创建一个空的
CSSStyleSheet
对象使用
CSSStyleSheet.replace()
或 CSSStyleSheet.replaceSync()
设置其内容通过将其赋给
ShadowRoot.adoptedStyleSheets
来添加到影子根 const sheet = new CSSStyleSheet();sheet.replaceSync("span { color: red; border: 2px dotted black;}");shadow.adoptedStyleSheets = [sheet];
声明式将一个影子Dom的
<style>
样式包含在 <template>
元素中添加到页面上。然后将该
<template>
元素添加到影子Dom上。 <body><template id="my-element"> <style> span { color: red; border: 2px dotted black; } </style></template></body><script>const template = document.getElementById("my-element"); shadow.appendChild(template.content);</script>
效果:
就像页面样式就像不会影响影子 DOM 中的元素一样,影子 DOM 样式也不会影响页面中其它元素的样式。
影子Dom和自定义元素
自定义元素结合使用影子Dom的优点:
保护自定义元素不被破坏为自定义元素提供自己的样式空间通常自定义元素本身是一个影子宿主,该元素在其根节点下创建多个元素以供元素的内部实现。
案例:创建一个绘制圆形的元素
<body> <filled-circle color="#d46b6b"></filled-circle></body><script> class FilledCircle extends HTMLElement { constructor() { super(); } connectedCallback() { // 创建一个影子根 // 自定义元素自身是影子宿主 const shadow = this.attachShadow({ mode: "open" }); // 创建内部实现 const svg = document.createElementNS( "http://www.w3.org/2000/svg", "svg" ); const circle = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); circle.setAttribute("cx", "50"); circle.setAttribute("cy", "50"); circle.setAttribute("r", "50"); circle.setAttribute("fill", this.getAttribute("color")); svg.appendChild(circle); shadow.appendChild(svg); } } customElements.define("filled-circle", FilledCircle);</script>
补充: 影子根ShadowRoot的相关属性和方法
ShadowRoot的携带很多属性和方法,可以参见https://developer.mozilla.org/zh-CN/docs/Web/API/ShadowRoot
host:ShadowRoot.host
,只读属性,返回对 ShadowRoot 所附加到的 DOM 元素的引用mode: ShadowRoot.mode
,只读属性,返回其模式打开或关闭。activeElement : Shadow.activeElement
只读属性,返回影子树中具有焦点的元素 等。
使用html模板
之前我们定义自定义元素的时候都是在js中编写对应的元素标签,这样写无疑是比较麻烦的,我们希望可以直接在html模板中编写对应的标签和样式,然后自定义组件直接用html模板中的标签定义自己的元素。
常见的使用方法有 <template>
和 <slot>
元素
template标签实现自定义元素
利用template标签不在页面中展示的效果实现自定义元素。
<template id="my-paragraph"> <p>My paragraph</p></template>
上面的代码不会展示在你的页面中,除非使用 JavaScript 获取它,然后添加到 DOM 中,如下面的代码:
let template = document.getElementById("my-paragraph");let templateContent = template.content;document.body.appendChild(templateContent);
因为template标签中的内容不会展示,自定义元素的时候就可以直接获取template标签中的内容使用,使用js将template标签的内容作为元素展示,并且不会影响其他内容的展示。
<body><template id="my-paragraph"> <style> p { color: white; background-color: #666; padding: 5px; } </style> <p>My paragraph</p></template><my-paragraph></my-paragraph></body><script>customElements.define( "my-paragraph", class extends HTMLElement { constructor() { super(); // 编写元素内容,这里采取将元素内容编写在html总然后直接获取插入的方法 let template = document.getElementById("my-paragraph"); let templateContent = template.content; // 创建一个影子根 const shadowRoot = this.attachShadow({ mode: "open" }); // 使用 Node.cloneNode() 方法添加了模板的拷贝到阴影的根结点上 shadowRoot.appendChild(templateContent.cloneNode(true)); } },);</script>
关键点是使用 Node.cloneNode()
方法添加了模板的拷贝到阴影的根结点上。
效果:
slots插槽实现自定义元素
slots插槽由其name
属性标识,并且允许你在模板中定义占位符
,当在标记中使用该元素时,该占位符可以填充所需的任何 HTML 标记片段。
<p><slot name="my-text">My default text</slot></p>
在标记中使用该元素 <my-paragraph> <span slot="my-text">Let's have some different text!</span></my-paragraph>
Let's have some different text!
会替换My default text
显示。
定义标签的时候slots插槽也需要依赖template标签实现,在template标签中使用slot插槽:
<body> <template id="my-paragraph"> <style> p { color: white; background-color: #666; padding: 5px; } </style> <p><slot name="my-text">My default text</slot></p> </template> <my-paragraph> </my-paragraph> <my-paragraph> <span slot="my-text">Let's have some different text!</span> </my-paragraph> <my-paragraph> <ul slot="my-text"> <li>Let's have some different text!</li> <li>In a list!</li> </ul> </my-paragraph> </body> <script> customElements.define( "my-paragraph", class extends HTMLElement { constructor() { super(); // 编写元素内容,这里采取将元素内容编写在html总然后直接获取插入的方法 let template = document.getElementById("my-paragraph"); let templateContent = template.content; // 创建一个影子根 const shadowRoot = this.attachShadow({ mode: "open" }); // 添加自定义元素到影子根 shadowRoot.appendChild(templateContent.cloneNode(true)); } } ); </script>
效果:
补充:插槽slot身上的属性和方法
详细参见: https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement
属性:
方法:
assign():将插槽的手动分配节点设置为一组有序的插槽表。assignedElements():返回分配给该槽(而不是其他节点)的元素序列。