探索 Web Components 的奇妙世界
web components 在 2018 年被纳入了 w3c 规范,但是直至现在,采用 web component 进行大规模开发的应用还很少。本文目的是介绍 Web components 的基本概念、技术要点和发展趋势,看下我们组件库可以怎么利用这个技术
I. 引言
如果你是原生 web 开发者,只使用 html/css/javascript 来开发 web 应用,那么你一定会遇到这样的问题:
1.
代码重复和维护困难:在大型项目中,经常需要编写大量重复的代码,例如相似的界面元素和交互逻辑。这不仅浪费时间,还增加了维护的成本和错误的风险。
2.
缺乏模块化和封装性:缺乏良好的模块化和封装,导致代码之间的关联性强,难以复用和维护。这限制了开发人员的灵活性。
3.
跨团队合作困难:不同团队可能同时开发自己的前端组件,但缺乏标准的组件规范和交互方式,导致协同和集成变得困难。
4.
难以实现一致的用户体验:前端界面经常需要在不同设备和屏幕上呈现,并适应不同的用户需求。但在传统开发中,实现一致的用户体验往往是一项挑战。
而如果你基于第三方框架或者库去开发 web 应用,你依然无法摆脱如上的一个或者多个问题,或​者会遇到框架/库使用的特定问题:
1.
跨技术栈调用:由于框架规则限制,你难以在不同的技术栈中调用基于框架能力开发的组件。同时即使实现跨技术栈,也会疲于处理框架升级兼容问题。
2.
运行时依赖过大:还是由于框架,你需要额外加载框架或者库的运行时代码依赖,这会明显增加用户的网络负担
为了解决这些问题,Web Components 应运而生。它是一种先进的前端技术,旨在提供可重用、封装性强、模块化的组件开发方式,使开发人员能够更高效地构建复杂的前端界面。Web Components 引入了一些新的概念和特性,如 Shadow DOM、HTML 模板和自定义元素,以支持组件的封装、可组合性、动态性和定制性。
我们先简单看下一个 Web Components, 然后我们再看它的核心技术组成
html
<!DOCTYPE html> <html> <head> <title>自定义按钮组件</title> </head> <body> <script> // 定义 ButtonComponent 组件类 class ButtonComponent extends HTMLElement { constructor() { super(); // 创建一个影子DOM this.shadowDOM = this.attachShadow({ mode: 'open' }); const sheet = new CSSStyleSheet(); sheet.replaceSync(`.custom-button{background-color: #FF0000;color: #FFFFFF;border: none;padding: 10px 20px;border-radius: 5px;font-size: 16px;outline: none;}`); this.shadowDOM.adoptedStyleSheets = [sheet]; // 创建一个按钮元素 this.button = document.createElement('button'); // 将按钮类名设置为自定义类名 this.button.className = 'custom-button'; // 为按钮设置默认属性 this.button.innerHTML = this.textContent || this.getAttribute('data-text') || '按钮'; // 拷贝按钮元素上的所有属性 Array.from(this.attributes).forEach(attr => { if (attr.name !== 'class') { this.button.setAttribute(attr.name, attr.value); } }); // 将按钮添加到影子DOM中 this.shadowDOM.appendChild(this.button); } // 监听按钮属性的变化 static get observedAttributes() { return ['disabled', 'type']; } // 处理属性变化 attributeChangedCallback(attr, oldValue, newValue) { if (attr === 'disabled') { if (newValue === 'true') { this.button.setAttribute('disabled', ''); } else { this.button.removeAttribute('disabled'); } } else if (attr === 'type') { this.button.setAttribute('type', newValue); } } } // 定义自定义元素 "button-component" customElements.define('button-component', ButtonComponent); </script> <!-- 使用自定义按钮组件 --> <button-component onclick="window.alert('hello')">自定义按钮</button-component> </body> </html>
具体效果可以在这里查看 W3Schools Tryit Editor
II. 核心技术
Web Components 为了实现组件的封装、可组合性、动态性和定制性,它由一些核心技术组成,包括以下几个关键概念:
1.
Custom Elements(自定义元素):Custom Elements 允许开发者创建自定义的 HTML 元素,并定义其行为和外观。通过继承 HTMLElement 类或其子类,开发者可以定义新的自定义元素,并将其添加到 Web 页面中。自定义元素可以封装复杂的功能,拥有自己的属性和方法,并具备与常规 HTML 元素类似的行为。
2.
Shadow DOM(影子 DOM):Shadow DOM 允许开发者封装自定义元素的内部结构和样式,并将其隔离在一个独立的 DOM 树中。通过使用影子 DOM,开发者可以防止外部样式和脚本对自定义元素产生干扰,并创建更加独立和可复用的组件。
3.
HTML Templates(HTML 模板):HTML Templates 允许开发者定义可复用的 HTML 结构,并在需要的时候进行实例化。通过使用 <template>​ 元素,开发者可以将一段 HTML 结构定义为模板,并通过 JavaScript 的 API 进行克隆、插入和操作。
另一个标准 HTML Imports (例如使用 <link rel="import" href="myfile.html >),已废弃不再详述。
Custom Elements
自定义元素核心是继承标准的 HTML 元素,根据继承的元素,可以划分两种类型:
自定义内置元素(Customized built-in element):继承自标准的 HTML 元素,如 HTMLButtonElement 或者其他标准 HTMLElement,通过改写和预设内置行为达到自定义元素目的
独立自定义元素(Autonomous custom element):继承 HTML 元素基类 HTMLElment,这样就就可以完全自定义元素行为了
先看一下 HTML 元素类的总体继承关系:
HTML DOM Hierarchy
自定义内置元素
通过继承 HTMLElement 的子类,扩展子类的能力或者预设行为。
如下例子,当继承 HTMLParagraphElement​ 创建自定义元素时,可以定义一个名为 FancyParagraph​ 的示例自定义元素,该元素继承自 <p>​ 元素,并添加一些额外的功能和样式。
html
<p is="fancy-paragraph"></p> <script> class FancyParagraph extends HTMLParagraphElement { constructor() { super(); // 初始化自定义元素的行为和样式 // 添加一些自定义样式 this.style.fontWeight = 'bold'; this.style.color = 'blue'; } connectedCallback() { // 元素被插入到文档时触发的事件,可以执行一些初始化逻辑 this.textContent = 'Hello, I am a fancy paragraph!'; } } // 将类注册为自定义元素 customElements.define('fancy-paragraph', FancyParagraph, { extends: 'p' }); </script>
在上面这个示例中,我们创建了一个名为 FancyParagraph​ 的继承类,它继承自 HTMLParagraphElement在构造函数中,我们可以对自定义元素进行初始化操作,例如设置额外的样式。在 connectedCallback()​ 方法中,我们添加了一些逻辑,当该元素被插入到文档时,将显示一段特殊的文本内容。
接着,我们使用 customElements.define()​ 方法将 FancyParagraph​ 类注册为自定义元素,并通过 extends: 'p'​ 来指定它扩展 <p>​ 元素。
最后在 HTML 页面中使用(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素) <p>(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素)​ 标签,并添加(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素) is(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素)​ 属性来标记使用了自定义的(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素) <fancy-paragraph>(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素)​ 元素。(请注意,和我们想象的使用方式不一样,这里还是使用了原生标签进行声明使用,但是使用 is 属性来标记使用自定义元素)
可以看到,继承内置元素可以享受自定义元素带来的额外功能和样式,同时仍然保留了 <p>​ 元素的基本特性。可以根据需要添加更多的功能和样式,使自定义元素更加丰富和有趣
独立自定义元素
通过扩展 HTMLElement 类来自定义独立元素,如下实例,创建一个 Popup 自定义元素
html
<div> <popup-info trigger="img" 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." ></popup-info> <script> // Create a class for the element class PopupInfo extends HTMLElement { constructor() { // Always call super first in constructor super(); } connectedCallback() { // Create a shadow root const shadow = this.attachShadow({ mode: "open" }); // Create spans 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"); // Take attribute content and put it inside the info span const text = this.getAttribute("data-text"); info.textContent = text; // Insert icon let triggerText; if (this.hasAttribute("trigger")) { triggerText = this.getAttribute("trigger"); } else { triggerText = "trigger"; } icon.textContent = triggerText // Create some CSS to apply to the shadow dom const 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; top: 20px; left: 10px; z-index: 3; } img { width: 1.2rem; } .icon:hover + .info, .icon:focus + .info { opacity: 1; } `; // Attach the created elements to the shadow dom shadow.appendChild(style); shadow.appendChild(wrapper); wrapper.appendChild(icon); wrapper.appendChild(info); } } // Define the new element customElements.define("popup-info", PopupInfo); </script> </div>
这个示例中,我们定义了一个名为 popup-info​ 的自定义元素,它具有一个 trigger​ 属性用于触发弹窗的打开状态。默认情况下,弹窗是关闭的
connectedCallback()​ 方法内部,我们定义了元素连接到 DOM 时元素将具有的所有功能。在这种情况下,我们将一个影子根附加到自定义元素,使用一些 DOM 操作来创建元素的影子 DOM 结构——然后将其附加到影子根——最后将一些 CSS 附加到影子根以进行样式设置。我们不在构造函数中执行这项工作,因为在连接到 DOM 之前,元素的属性是不可用的。
最后,我们使用前面提到的 define()​ 方法在 CustomElementRegistry​ 中注册我们的自定义元素——在参数中,我们指定元素名称,然后定义其功能的类名称:
独立自定义元素,则直接使用我们定义的 HTML 标签进行使用​
可以看到,其实两种类型的自定义元素区别不大,有两个注意的地方:
使用 define 定义自定义标签的时候,为了避免与原有标签冲突,不能使用单个单词强制使用短横线连接
定制元素标签不能是自闭合的,因为 HTML 只允许一部分元素可以自闭合。
接下来我们深入去看一下细节
构造函数
应该注意到,无论是哪种类型的自定义元素,都包含构造函数,并且必须是构造函数主题的第一个语句,建立正确的原型链。在 Web Components 的规范 中,明确了自定义元素构造函数和交互行为的要求
对 super() 的无参数调用必须是构造函数主体中的第一个语句,以便在运行任何进一步的代码之前建立正确的原型链和此值。
return 语句不得出现在构造函数体内的任何位置,除非它是简单的早期返回(return 或 return this)。
构造函数不得使用 document.write() 或 document.open() 方法。
不得检查元素的属性和子元素,因为在非升级情况下不会出现任何属性和子元素,并且依赖升级会降低元素的可用性。
该元素不得获得任何属性或子元素,因为这违反了使用 createElement 或 createElementNS 方法的使用者的期望。
一般来说,工作应该尽可能推迟到connectedCallback——尤其是涉及获取资源或渲染的工作。但是,请注意,connectedCallback 可以被多次调用,因此任何真正一次性的初始化工作都需要一个防护来防止它运行两次。
一般来说,构造函数应该用于设置初始状态和默认值,并设置事件侦听器和可能的影子根。
javascript
// 自定义元素最小实现 class WordCount extends HTMLParagraphElement { constructor() { super(); } // 此处编写元素功能 } // 独立自定义元素最小实现 class PopupInfo extends HTMLElement { constructor() { super(); } // 此处编写元素功能 }
生命周期
如果自定义元素被注册,页面中的代码与自定义元素交互的时候,浏览器会调用类的一些方法,这些方法就是自定义元素的生命周期:
connectedCallback()​:每当元素添加到文档中时调用。规范建议开发人员尽可能在此回调中实现自定义元素的设定,而不是在构造函数中实现。
disconnectedCallback()​:每当元素从文档中移除时调用。
adoptedCallback()​:每当元素被移动到新文档中时调用。
attributeChangedCallback()​:在属性更改、添加、移除或替换时调用。
响应属性变化的实现包含两个部分:
1.
一个名为 observedAttributes​ 的静态属性。这必须是一个包含元素需要变更通知的所有属性名称的数组。
2.
attributeChangedCallback()​ 生命周期回调的实现。
javascript
// 为这个元素创建类 class MyCustomElement extends HTMLElement { static observedAttributes = ["color", "size"]; constructor() { // 必须首先调用 super 方法 super(); } connectedCallback() { console.log("自定义元素添加至页面。"); } disconnectedCallback() { console.log("自定义元素从页面中移除。"); } adoptedCallback() { console.log("自定义元素移动至新页面。"); } attributeChangedCallback(name, oldValue, newValue) { console.log(`属性 ${name} 已变更。`); } }
注册自定义元素
通过继承定义好自定义元素后,通过 CustomElementRegistry 接口对自定义元素进行注册
javascript
customElements.define(name, constructor, options)
CustomElementRegistry.define() 支持如下参数:
name: 元素名称
ebnf
PotentialCustomElementName ::= [a-z] (PCENChar)* '-' (PCENChar)* PCENChar ::= "-" | "." | [0-9] | "_" | [a-z] | #xB7 | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x203F-#x2040] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
constructor: 自定义元素构造器。
options: 可选参数​,控制元素如何定义。目前有一个选项支持:
extends. 指定继承的已创建的元素。被用于创建自定义元素。
CustomElementRegistry 除了 define 方法外还有如下常用方法:
get(name: string): 返回指定自定义元素的构造函数,如果未定义自定义元素,则返回undefined
javascript
customElements.define( "my-paragraph", class extends HTMLElement { constructor() { super(); let template = document.getElementById("my-paragraph"); let templateContent = template.content; const shadowRoot = this.attachShadow({ mode: "open" }).appendChild( templateContent.cloneNode(true), ); } }, ); let ctor = customElements.get("my-paragraph");
getName(constructor: HTMLElement): 返回构造函数注册的自定义元素名称
javascript
class MyParagraph extends HTMLElement { constructor() { let templateContent = document.getElementById("my-paragraph").content; super() // returns element this scope .attachShadow({ mode: "open" }) // sets AND returns this.shadowRoot .append(templateContent.cloneNode(true)); } } customElements.define("my-paragraph", MyParagraph); customElements.getName(MyParagraph) === "my-paragraph";
upgrade(dom: Element): 将更新节点子树中所有包含阴影的自定义元素,甚至在它们连接到主文档之前也是如此。
javascript
const el = document.createElement("spider-man"); class SpiderMan extends HTMLElement {} customElements.define("spider-man", SpiderMan); console.assert(!(el instanceof SpiderMan)); // 未更新 customElements.upgrade(el); console.assert(el instanceof SpiderMan); // 已更新
whenDefined(name: string): 当元素被注册的时候, 该方法返回一个 promise
javascript
// 定义一个自定义元素 class MyCustomElement extends HTMLElement { // ... } // 注册自定义元素 customElements.define('my-custom-element', MyCustomElement); // 使用 whenDefined() 方法来检测自定义元素是否已被定义 customElements.whenDefined('my-custom-element').then(() => { console.log('my-custom-element 已被定义'); // 在自定义元素定义完成后执行的逻辑 });
此外,还有 CustomStateSet 接口,允许开发人员添加和删除自主自定义元素的状态(但不能从内置元素派生的元素)。然后,这些状态可以用作自定义状态伪类选择器,其方式与内置元素的伪类类似。但不同浏览器实现可能差异较大,这里不多深入了,有兴趣的可以自行去查看
Shadow DOM
自定义元素能够很好地封装和重用功能模块,为了能够让自定义元素正常工作,重要的是让运行中的代码不应该被修改自定义元素的内部实现。
Shadow DOM 就是这样一种用于创建封装和隔离 Web 组件的技术。它允许您将组件的样式和 DOM 结构封装在一个独立的 Shadow DOM 树中,与外部文档的 DOM 树分隔开来。解决 Web 开发中的样式和命名冲突问题,并提供更好的组件化和封装性。
具体来说,Shadow DOM 允许将隐藏的 DOM 树附加到常规 DOM 树中的元素上——这个 Shadow DOM 始于一个 shadowRoot,在其之下你可以用与普通 DOM 相同的方式附加任何元素,例如添加子节点和设置属性、使用 element.style.xxx 对单个节点进行样式设置,或将整个影子树内的样式添加到一个 <style>​ 元素中。不同之处在于 shadow DOM 内的所有代码都不会影响它的外部,从而便于实现封装。
shadowdom
在更加深入前,了解一下基本的术语:
shadow host: shadow DOM 附加到的常规的 DOM 节点
shadow tree: shadow DOM 内部的 DOM 树
shadow boundary: shadow DOM 终止,常规 DOM 开始的地方
shadow root: shadow tree 的根节点
此外,在 shadow DOM 通过 Web Components 向我们开放之前,其实已经使用它来封装元素来,比如 video 元素
创建 shadow DOM
下面通过两个元素进行比对
html
<div id="host"></div> <span>I'm not in the shadow DOM</span>
将 "#host" 元素作为 shadow host。通过 host 元素上面的 attachShadow 来创建 shadow DOM
javascript
const host = document.querySelector("#host"); const shadow = host.attachShadow({ mode: "open" }); const span = document.createElement("span"); span.textContent = "I'm in the shadow DOM"; shadow.appendChild(span);
但是!!!!并非所有 HTML 元素都可以开启 shadow DOM,例如用 img 这样的非容器素作为 shadow Host 不合理,而且会报错。目前支持的元素: article、 body、h1 ~ h6、header、 p、 aside、 div、aside、nav、span、section、main、footer、blockquote。
Javascript 交互
在创建的 shadow DOM 的时候,我们用了 attachShadow​ 的方法,传参为 { mode:'open' }​,这表明 Javascript 可以通过访问 shadow host 访问 shadow DOM 内部。
attachShadow()​ 会返回对 shadowRoot 的引用
javascript
host.attachShadow({ mode: "open" }).querySelectorAll("span")
也可以通过访问 shadow host 节点上的 shadowRoot 属性可以获得 shadow DOM 的引用,这样既可以使用 javascript 对其内部进行操作
javascript
host.shadowRoot.querySelectorAll("span")
使用 {mode: "open"}​ 参数为页面提供一种破坏影子 DOM 封装的方法。如果你不希望给页面这个能力,传递 {mode: "closed"}​ 作为替代,此时 shadowRoot​ 返回 null​。
注意:即使使用 {mode: "closed"}​ 作为替代,也可以通过浏览器插件绕过这个机制
对于事件交互,在 web component 中,可以使用如下方式:
1.
添加事件监听器:可以使用 addEventListener​ 方法在 Web Components 内部添加事件监听器。例如,可以通过shadowRoot.addEventListener('click', handler)​来监听点击事件,并在事件发生时调用相应的处理函数
2.
自定义事件:Web Components 可以定义自己的事件,以便在组件内部进行通信。可以使用CustomEvent​构造函数创建自定义事件,并使用dispatchEvent​方法触发事件。其他组件可以通过添加事件监听器来捕获这些自定义事件
3.
属性和属性变化事件:Web Components 可以定义属性,并在属性值发生变化时触发相应的事件。可以使用attributeChangedCallback​方法来监听属性变化事件,并在属性值发生变化时执行相应的处理逻辑。
4.
冒泡和捕获阶段:Web Components 中的事件处理遵循事件冒泡和捕获的机制。事件冒泡是指事件从触发元素向上层元素传递的过程,而事件捕获是指事件从上层元素向触发元素传递的过程。可以使用 addEventListener​ 方法的第三个参数来指定事件监听器是在冒泡阶段还是捕获阶段触发
样式封装
Shadow DOM 对外部有一个很好的隔离。内部的样式有两种方式进行定义:
编程式:即通过 javascript 构建 CSSStyleSheet 对象并将其附加到 shadow root
声明式:通过模板在一个 template 元素中添加 style 元素
1.
编程式例子如下:
创建一个空的 CSSStyleSheet​ 对象
通过将其赋给 ShadowRoot.adoptedStyleSheets (en-US) 来添加到影子根
javascript
const sheet = new CSSStyleSheet(); sheet.replaceSync("span { color: red; border: 2px dotted black;}"); const host = document.querySelector("#host"); const shadow = host.attachShadow({ mode: "open" }); shadow.adoptedStyleSheets = [sheet]; const span = document.createElement("span"); span.textContent = "I'm in the shadow DOM"; shadow.appendChild(span);
更甚者你可以通过 innerHTML 的暴力方式进行插入
2.
声明式则相对来说具有更好的阅读性,主要是利用 template 元素的能力
正常定义 html 文件,只不过把内容包裹在 template 标签下
html
<template id="my-element"> <style> span { color: red; border: 2px dotted black; } </style> <span>I'm in the shadow DOM</span> </template> <div id="host"></div> <span>I'm not in the shadow DOM</span>
然后再通过 shadowRoot 插入到 shadow DOM 中
javascript
const host = document.querySelector("#host"); const shadow = host.attachShadow({ mode: "open" }); const template = document.getElementById("my-element"); shadow.appendChild(template.content);
两种方式的选择要在维护性和灵活性下面做选择:
创建一个 CSSStyleSheet​ 并通过 adoptedStyleSheets​ 将其赋给 shadow root 允许你创建单一样式表并将其与多个 DOM 树共享。例如,一个组件库可以创建单个样式表,然后将其与该库的所有自定义元素共享。浏览器将仅解析该样式表。此外,你可以对样式表进行动态更改,并将更改传播到使用表的所有组件。
而当你希望是声明式的、需要较少的样式并且不需要在不同组件之间共享样式的时候,附加 <style>​ 元素的方法则非常适合。
自定义标签的样式可以直接在全局定义
template & slot
template 元素标签
上面已经介绍了简单提及了 <template>​ 标签元素的复用能力。
<template>标签是 HTML 中存在的,但在 template​ 标签被激活前:
标签会被解析,但不会被渲染,标签的内容也是会被隐藏 ,页面上看不到标签展示效果
模板里的内容不会有副作用,例如 script 标签里不的脚本不会执行,图片不会加载, 视频不会播放
基本上可以放置于任何节点上,例如 header、body 等;激活一个 template​ 最简单的方式是对它的内容做个深拷贝,然后再插入节点中
声明定义:
html
<template id="tpl"> <style> p { color: white; background-color: #666; padding: 5px; } </style> <p>我的段落</p> </template>
简单实用 template
javascript
// get element let divRoot = document.querySelector("#root"); let tpl = document.querySelector("#tpl").content; divRoot.appendChild(document.importNode(tpl, true));
或者使用 template 作为 shadow DOM 的内容
javascript
customElements.define( "my-paragraph", class extends HTMLElement { constructor() { super(); let template = document.getElementById("tpl"); let templateContent = template.content; const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(templateContent.cloneNode(true)); } }, );
在浏览器中通过开发者工具检查网页内容时,可以看到 <template>​ 标签中渲染的节点内容 是基于 DocumentFragment,而 DocumentFragment 也是批量向 HTML 中添加元素的高效工具,此时的 DocumentFragment 就像一个对应子树的最小化 document 对象,也就是说,如果需要操作 <template>​ 标签中节点,必须要先获取对应 DocumentFragment 的引用,即 document.querySelector('#tpl').content
image
slot
模板本身已经很有用了,但是如果能够让 template 支持外部传入 DOM 片段,具体来说就是在组件内部定义可插入内容的插槽,并将外部传入的内容插入到相应的插槽中,那灵活度就直接拉满了。
要实现这个能力,也就是说位于 shadow host 中的 HTML 需要一种机制以渲染到 shadow DOM中去,但这些 HTML 又不需要存在于shadow DOM 树中。这个机制包括:
shadow DOM 需要具有更高的优先级
javascript
document.body.innerHTML = ` <div id="foo"> <h1>I'm foo's child</h1> </div> `; const foo = document.querySelector('#foo'); const openShadowDOM = foo.attachShadow({ mode: 'open' }); // 为影子 DOM 添加内容 openShadowDOM.innerHTML = ` <p>this is openShadowDOM content</p> `
可以看到 shadowDOM 是渲染页面上了,虽然 DOM Tree 依然保留了 <h1>I'm foo's child</h1>​ 但并没有被渲染 ​ image
<slot> 标签:指示浏览器在哪里放置我们的 HTML 片段
html
<!DOCTYPE html> <html> <head> <title>Page Title</title> </head> <body> <h1>This is a Heading</h1> <p>This is a paragraph.</p> <div id="root"></div> <script> const root = document.querySelector("#root") root.innerHTML = ` <div id="foo"> <h1>I'm foo's child</h1> </div> `; const foo = root.querySelector('#foo'); const openShadowDOM = foo.attachShadow({ mode: 'open' }); // 为影子 DOM 添加内容 openShadowDOM.innerHTML = ` <p>this is openShadowDOM content</p> <slot></slot>` </script> </body> </html>
image
命名 slot:通过为 slot 命名,能够支持多个槽位
html
<body> <h1>This is a Heading</h1> <div id="root"></div> <script> const root = document.querySelector("#root") root.innerHTML = ` <div id="foo"> <h1 slot="foo">I'm foo's child</h1> </div> `; const foo = root.querySelector('#foo'); const openShadowDOM = foo.attachShadow({ mode: 'open' }); // 为影子 DOM 添加内容 openShadowDOM.innerHTML = ` <slot name="bar"></slot> <slot name="foo"></slot>` </script> </body>
image
同时也需要注意:通过插槽的方式插入到 shadow DOM 中的节点,依然能够通过 document 访问到,而通过 shadowRoot 反而无法访问
III. 发展趋势
目前 web components 的标准还在不断发展,自 v1 发布到所有主要浏览器以来的三年里,web component 下的功能数量几乎增加了一倍。下面是各种已发布、正在进行和计划的 web component 相关标准的图表。
image
参照 chrome 平台 上的数据,web component 的发展正在逐步向上
image
同时,在 mobile 端的占比相对 desktop 端更高:
image
而具体到 全局的数据,看着就不那么好看了
image
不过看着吓人,实际很多标红的是由于 html import 被废弃了,所以厂商就都不支持了。
也有一些第三方统计了 web component 相对其他 web 框架的性能和体积
https://webcomponents.dev/blog/all-the-ways-to-make-a-web-component/#bundle-size
劣势
1.
兼容性问题:尽管现代浏览器已经支持Web组件规范,但是在一些旧版浏览器中可能存在兼容性问题。这可能需要开发者在使用Web组件时进行额外的兼容性处理或提供替代方案。
2.
生态系统支持不足:尽管Web组件在现代浏览器中得到了广泛支持,但与其他流行的前端框架相比,Web组件的生态系统相对较小。这意味着可能没有像React、Angular或Vue.js那样丰富的社区支持、第三方库和插件。
优点
原生技术:由于Web Component是浏览器原生支持的技术,它们可以在任何现代浏览器中使用,而不需要额外的库或框架。这使得你的Web Components可以与其他人的组件或库进行良好的互操作。你可以在Vue项目的某个页面使用Web Component,然后在Angular项目的另一个页面中使用相同的组件,不需要任何额外的工作
性能优势
尽管有关性能测试显示 ShadowDOM 因为包含更多信息,创建起来会比原生 DOM节点慢很多,API 命令式调用更会增加时间消耗。但 CustomElement 的 upgrade 机制和 HTML Templates 保证了组件内部逻辑并不需要立即执行,相比于普通的 React 组件需要走完所有必要节点逻辑,Web Component 的真实渲染性能是更好的。
同时结合异步渲染、SSR,Web Component 可以轻松完成各种性能优化。
Template 相比 JSX 会产生更小的内存占用,更少的 CPU 运算负担。
封装性:通过使用影子DOM,Web Component 可以将组件的样式和结构封装在组件内部,与外部环境隔离。这意味着你编写的组件中的样式和HTML结构不会被外部代码所干扰。这种封装性使得你的组件更加健壮、可维护性更高,并且不容易受到外部样式的意外更改。
面向未来:Web组件是面向未来的技术,它们在Web开发中具有巨大的潜力
IV. 应用
行业内目前的应用主要有几个方向:
基础组件库,通过标准化降低开发者的接入门槛
应用框架或打造设计系统,改造优化自己的应用架构、统一多产品线的体验
渐进式的技术升级迭代,跨框架组件共享,降低风险
Youtube
YouTube 的 Google 旗下的产品,web component 是 Google 最早提出来的,后来发展成了标准,YouTube 是最早采用 Web Component 技术的应用之一,多年来一直使用这种技术构建其界面。检查源代码,你会看到各种自定义元素,从 ytd-video-preview​ 到 iron-ally-announcer​。现在应该是全站都用了 web component 了
image
Microsoft 多系列产品
几年前,微软使用基于 FAST 的 Web Component 重构了 MSN,这将性能提高了 30% 到 50%,比之前使用 React 构建的版本性能更好,还有新版 Edge 部件应用
image
Adobe Photoshop
Adobe 使用 Lit 将 Photoshop 带到了浏览器中。它现在还处于 beta 版本,如果你是 Adobe 的订阅用户,可以自行尝试。整个应用程序中有很多自定义元素,从构成应用程序根的 psw-app​,到像 psw-layers-panel​ 这样的 shell 元素,再到像 sp-action-button​ 这样的 UI 组件。
image
社区生态
Polymer:2013年就开始了!!由于起步几乎最早以及 google 背书,可能到现在也是影响用户数最大的 Web Components 基础库,Youtube 基于 Polymer 对整站做了重构,Google 很多产品包括 Android 和 ChromeOS 平台也都用了 Polymer。
Lit: 还是 Google!!!随着 Polymer 的轻量化升级,于是在 2018 年又发布了更现代化的 lit,Google 推荐新用户使用 lit,面向现代化 JS 语法
Omi: 腾讯开源的基于 web components 的框架,面向对象编程(OOP) 和 数据驱动编程(DOP) 两种范式都支持
Stencil: 一个 web components 编译器
主流框架支持
Vue 3.0 开始支持 Web Components,Vue3.2 开始支持单文件组件创建 CustomElements
React 在实验版本中支持了 CustomElements
Angular 的支持比前两位更好。
Svelte 则是生来就支持 CustomElements ,并提供了编译 API
V. 总结
Web component 是一种用于构建可重用的自定义HTML元素的技术。它由三个主要技术组成:custom element、shadow DOM 和 HTML template。
自定义元素允许开发者创建自己的HTML元素,这些元素可以像内置的HTML元素一样在网页中使用。通过使用自定义元素,开发者可以封装一些特定的功能和样式,并将其作为单个组件在不同的页面中重复使用。
shadow DOM 是一种用于封装组件样式和结构的技术。它允许开发者将组件的样式和结构隔离起来,以防止它们与页面上的其他元素发生冲突。影子DOM使得组件的样式和结构可以独立于页面的全局样式。
HTML template 允许开发者定义组件的结构。通过使用HTML模板,开发者可以在组件中定义一些初始的HTML结构,然后在使用组件时填充这些结构。HTML模板提供了一种简单和可重用的方式来定义组件的结构。
Web组件的主要优点包括:
1.
可重用性:Web组件可以在不同的项目和页面中重复使用,提高了代码的可维护性和开发效率。
2.
封装性:Web组件允许开发者将组件的样式和结构封装在一起,避免了与页面上其他元素的冲突。
3.
独立性:Web组件的样式和结构可以独立于页面的全局样式,使得组件可以在不同的环境中使用而不受影响。
4.
可扩展性:Web组件可以通过添加新的自定义元素和功能来扩展,使得组件可以适应不同的需求。
然而,Web组件也有一些限制和挑​战,包括浏览器兼容性问题、学习曲线较陡峭以及对构建工具和框架的依赖性等。不过,随着Web组件的不断发展和浏览器的支持改善,它已经成为构建现代Web应用程序的有力工具之一。
VI. 参考
粤ICP备2024250534号-1