2019 年第一次了解到 Web Components 这个概念,直到最近才开始尝试使用,这篇将简单介绍 Web Components,了解它的标准,解决什么问题以及它的优势,它提供的接口 API、兼容程度、如何使用它写一个简单的组件等。
随着前端框架的流行,组件化开发已经趋于常态,我们通常会把功能通用的模块抽取然后封装成单个组件,这样使用和维护起来都会变得更加简单。但组件也受限于框架,例如一旦离开框架本身,组件就无法使用了,那有没有跨越框架范围的技术构建通用的组件呢?有的,就是今天要介绍的主角 Web Components。
Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps.
Web Components 是一套 Web API,允许你创建能在 Web 页面和应用中使用的自定义、可重用、封装的 HTML 标签。总体上来说 Web Components 是 “通过一种标准化的非侵入的方式封装一个组件”。Web Components 的概念最早由 Alex Russell 在2011年的 Fronteers大会上首次提出,2013年 Google 发布了 Polymer 框架,是基于 Web Components API 的实现,来推动 Web Components 的标准化。 2014 年的时候 Chrome 发布了早期的 v0 级别的组件规范,目前已更新到 v1 版本,被各大浏览器接受并支持。
标准化
w3c 也不断在为 web 标准规范做努力,其中就包括 Web Components, 这套 API 规范成为标准被绝大多数浏览器支持后,我们就能开发更通用的组件了,不用花时间在框架的选择上,而是更聚焦在组件本身,通过 HTML、CSS、JS 来构建原生组件将会成为未来的前端标准。
非侵入式
侵入性是指设计时的组件耦合太强了,引入这个组件导致其它代码或者设计要做相应的更改来适应新组件,而非侵入式的组件没有过多的依赖,方便迁移至其他地方。Web Components 组件能够很好的组织好自身的 HTML 结构、 CSS 样式、JS 代码,而且不会干扰到页面中的其他代码。
不依赖第三方库或框架
Web Components 可以在不需要引入第三方的库或者框架的情况下通过浏览器的这套 API 创建可复用的组件,也可以和任意与 HTML 交互的 JavaScript 库和框架搭配使用。
HTML 内的 DOM 模板,在 template 元素内声明,内联样式 style 需要放置在它的内部,模板技术引入了两个重要的元素 template 和 slot ,template 提供模板的功能,slot 则被用来提供一个占位符hao,使 template 更灵活。
template 标签本质上合 HTML 内置标签是一样的,但在 template 标签被激活前:
html
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7</head> 8<body> 9 <h3>Web Components</h3> 10 <h3> example - 1</h3> 11 <template id="mytemplate"> 12 <img src="" alt="image"> 13 <div class="comment"></div> 14 <script> 15 console.log('template') 16 </script> 17 </template> 18</body> 19<script src="./index.js"></script> 20</html> 21
javascript
1// index.js 2var t = document.querySelector('#mytemplate'); 3t.content.querySelector('img').src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c7b5bd4445364f3887f7b708c812ca48~tplv-k3u1fbpfcp-zoom-1.image'; 4var clone = document.importNode(t.content, true); 5document.body.appendChild(clone);
用户信息卡片及 Slot 的实例:
html
1<!-- html --> 2<!DOCTYPE html> 3<html lang="en"> 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8</head> 9<body> 10 <h3>Web Components</h3> 11 <template id="profile-tpl"> 12 <div class="profile-name"></div> 13 <img src="" class="profile-img"> 14 <style> 15 :host { 16 display: block; 17 border: 1px solid red; 18 } 19 img { 20 max-width: 100px; 21 border-radius: 50%; 22 border: 1px solid seagreen; 23 } 24 </style> 25 </template> 26</body> 27<script src="./index.js"></script> 28</html>
javascript
1// index.js 2let template = document.querySelector('#profile-tpl'); 3template.content.querySelector('.profile-img').src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bec4077911a44e789fa163ac05b3a18~tplv-k3u1fbpfcp-zoom-1.image'; 4template.content.querySelector('.profile-name').textContent = 'bytedance'; 5document.body.appendChild(template.content);
html
1<!-- slot例子 --> 2<p><slot name="my-tpl">default text</slot></p> 3<my-template> 4 <span slot="my-tpl">Let's have some different text!</span> 5</my-template>
Custom Elements
定义新的元素标签,可以被解析成 HTML。定义时首先需要声明一个类,这个类需要继承 HTMLElement 类,这样能够使用组件的一些生命周期回调函数,这些函数帮助我们增强组件的能力。总结一下要点:
生命周期回调函数:
执行顺序(这里 attributeChangedCallback 在前面是因为需要调整配置,应该在插入 DOM 之前完成):
javascript
1constructor -> attributeChangedCallback -> connectedCallback
用自定义标签的方式来实现一个用户卡片(user-card)的例子:
html
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7</head> 8<body> 9 <h3>Web Components</h3> 10 <user-card></user-card> 11 <!-- <foo-bar></foo-bar> --> 12</body> 13<script src="./index.js"></script> 14</html> 15
javascript
1// index.js 2class UserCard extends HTMLElement { 3 constructor() { 4 super(); 5 this.innerHTML = 'user-card'; 6 } 7} 8window.customElements.define('user-card', UserCard);
这里的 Shaodow DOM 不应该跟常用的几个框架中的 Virtual DOM 混淆(Virtual DOM 主要是做性能层的优化),Shadow DOM 让我们能够创建一套完全独立于其它元素的 DOM 树,也叫“影子DOM”,有了它可以保证当前的这个组件是个具备独立功能的组件,与其它DOM元素互不干扰。跟 iframe 相似,是一个独立是沙盒,但它没有自己的 window,有一个轻量级 document,另外 shadowRoot 对象不支持所有的 DOM API ,支持主流的 getElementById、querySelector 和 querySelectorAll 等方法


结构:
Element.attachShadow() 方法会将 shadow DOM 树附加给特定元素,并返回它的 ShadowRoot。该方法只有一个对象类型,一个 Key 值 mode,可以设置为 open 或 closed 来指定该模式的打开和关闭。open 状态表示可以通过 JavaScript 来获取 Shadow DOM,close 状态 shadowRoot 将会返回 null。
javascript
1let shadow = elementRef.attachShadow({mode: 'open'}); 2let myShadowDom = myCustomElem.shadowRoot;
例子:
javascript
1window.customElements.define('user-card', UserCard); 2class FooBar extends HTMLElement { 3 constructor() { 4 super(); 5 this.attachShadow({ mode: 'open' }); 6 this.innerHTML = 'foo-bar'; 7 } 8 connectedCallback() { 9 this.shadowRoot.innerHTML = ` 10 <p>I'm in the Shadow Root!</p> 11 `; 12 } 13} 14window.customElements.define('foo-bar', FooBar); 15
并非所有 HTML 元素都可以开启 Shadow DOM,例如用 img 这样的非容器素作为 Shadow Host 不合理,而且会报错。目前支持的元素: article、 body、h1 ~ h6、header、 p、 aside、 div、aside、nav、span、section、main、footer、blockquote。
javascript
1document.createElement('img').attachShadow({mode: 'open'}); 2// => DOMException
另一个标准 HTML Imports (例如使用 <link rel="import" href="myfile.html >),已废弃不再详述。
2016 年 Safari 开始支持 Custom Elements 和 Shadow Dom,Firefox 则是在 2017 年跟进,目前各 API 兼容性如下:




css
1<style> 2 user-card { 3 border: 1px solid red; 4 } 5</style> 6
与目前其它框架的比较
组件传值监听和事件绑定:
javascript
1class CustomComponent extends HTMLElement { 2 static get observedAttributes() { 3 return ["attributesName"]; 4 } attributeChangedCallback(name, oldValue, newValue) { 5 // 当属性值变更时做一些操作 6 } 7}
javascript
1class Button extends HTMLElement { 2 this.$btn = this._shadowRoot.querySelector('button'); 3 this.$btn.addEventListener('click', () => {} 4}
javascript
1class ClickCounter extends HTMLElement { 2 constructor() { 3 super(); 4 5 this._timesClicked = 0; 6 7 var button = document.createElement("button"); 8 button.textContent = "Click me"; 9 button.onclick = (evt) => { 10 this._timesClicked++; 11 this.dispatchEvent(new CustomEvent("clicked", { 12 detail: this._timesClicked 13 })); 14 }; 15 16 this.append(button); 17 } 18}; 19customElements.define("click-counter", ClickCounter); 20var counter = document.querySelector("click-counter"); 21counter.addEventListener("clicked", (evt) => { 22 console.log(evt.detail); 23}); 24
2019 年第一次了解到 Web Components 这个概念,直到最近才开始尝试使用,这篇将简单介绍 Web Components,了解它的标准,解决什么问题以及它的优势,它提供的接口 API、兼容程度、如何使用它写一个简单的组件等。
随着前端框架的流行,组件化开发已经趋于常态,我们通常会把功能通用的模块抽取然后封装成单个组件,这样使用和维护起来都会变得更加简单。但组件也受限于框架,例如一旦离开框架本身,组件就无法使用了,那有没有跨越框架范围的技术构建通用的组件呢?有的,就是今天要介绍的主角 Web Components。
Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps.
Web Components 是一套 Web API,允许你创建能在 Web 页面和应用中使用的自定义、可重用、封装的 HTML 标签。总体上来说 Web Components 是 “通过一种标准化的非侵入的方式封装一个组件”。Web Components 的概念最早由 Alex Russell 在2011年的 Fronteers大会上首次提出,2013年 Google 发布了 Polymer 框架,是基于 Web Components API 的实现,来推动 Web Components 的标准化。 2014 年的时候 Chrome 发布了早期的 v0 级别的组件规范,目前已更新到 v1 版本,被各大浏览器接受并支持。
标准化
w3c 也不断在为 web 标准规范做努力,其中就包括 Web Components, 这套 API 规范成为标准被绝大多数浏览器支持后,我们就能开发更通用的组件了,不用花时间在框架的选择上,而是更聚焦在组件本身,通过 HTML、CSS、JS 来构建原生组件将会成为未来的前端标准。
非侵入式
侵入性是指设计时的组件耦合太强了,引入这个组件导致其它代码或者设计要做相应的更改来适应新组件,而非侵入式的组件没有过多的依赖,方便迁移至其他地方。Web Components 组件能够很好的组织好自身的 HTML 结构、 CSS 样式、JS 代码,而且不会干扰到页面中的其他代码。
不依赖第三方库或框架
Web Components 可以在不需要引入第三方的库或者框架的情况下通过浏览器的这套 API 创建可复用的组件,也可以和任意与 HTML 交互的 JavaScript 库和框架搭配使用。
HTML 内的 DOM 模板,在 template 元素内声明,内联样式 style 需要放置在它的内部,模板技术引入了两个重要的元素 template 和 slot ,template 提供模板的功能,slot 则被用来提供一个占位符hao,使 template 更灵活。
template 标签本质上合 HTML 内置标签是一样的,但在 template 标签被激活前:
html
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7</head> 8<body> 9 <h3>Web Components</h3> 10 <h3> example - 1</h3> 11 <template id="mytemplate"> 12 <img src="" alt="image"> 13 <div class="comment"></div> 14 <script> 15 console.log('template') 16 </script> 17 </template> 18</body> 19<script src="./index.js"></script> 20</html> 21
javascript
1// index.js 2var t = document.querySelector('#mytemplate'); 3t.content.querySelector('img').src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c7b5bd4445364f3887f7b708c812ca48~tplv-k3u1fbpfcp-zoom-1.image'; 4var clone = document.importNode(t.content, true); 5document.body.appendChild(clone);
用户信息卡片及 Slot 的实例:
html
1<!-- html --> 2<!DOCTYPE html> 3<html lang="en"> 4<head> 5 <meta charset="UTF-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 <title>Document</title> 8</head> 9<body> 10 <h3>Web Components</h3> 11 <template id="profile-tpl"> 12 <div class="profile-name"></div> 13 <img src="" class="profile-img"> 14 <style> 15 :host { 16 display: block; 17 border: 1px solid red; 18 } 19 img { 20 max-width: 100px; 21 border-radius: 50%; 22 border: 1px solid seagreen; 23 } 24 </style> 25 </template> 26</body> 27<script src="./index.js"></script> 28</html>
javascript
1// index.js 2let template = document.querySelector('#profile-tpl'); 3template.content.querySelector('.profile-img').src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bec4077911a44e789fa163ac05b3a18~tplv-k3u1fbpfcp-zoom-1.image'; 4template.content.querySelector('.profile-name').textContent = 'bytedance'; 5document.body.appendChild(template.content);
html
1<!-- slot例子 --> 2<p><slot name="my-tpl">default text</slot></p> 3<my-template> 4 <span slot="my-tpl">Let's have some different text!</span> 5</my-template>
Custom Elements
定义新的元素标签,可以被解析成 HTML。定义时首先需要声明一个类,这个类需要继承 HTMLElement 类,这样能够使用组件的一些生命周期回调函数,这些函数帮助我们增强组件的能力。总结一下要点:
生命周期回调函数:
执行顺序(这里 attributeChangedCallback 在前面是因为需要调整配置,应该在插入 DOM 之前完成):
javascript
1constructor -> attributeChangedCallback -> connectedCallback
用自定义标签的方式来实现一个用户卡片(user-card)的例子:
html
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7</head> 8<body> 9 <h3>Web Components</h3> 10 <user-card></user-card> 11 <!-- <foo-bar></foo-bar> --> 12</body> 13<script src="./index.js"></script> 14</html> 15
javascript
1// index.js 2class UserCard extends HTMLElement { 3 constructor() { 4 super(); 5 this.innerHTML = 'user-card'; 6 } 7} 8window.customElements.define('user-card', UserCard);
这里的 Shaodow DOM 不应该跟常用的几个框架中的 Virtual DOM 混淆(Virtual DOM 主要是做性能层的优化),Shadow DOM 让我们能够创建一套完全独立于其它元素的 DOM 树,也叫“影子DOM”,有了它可以保证当前的这个组件是个具备独立功能的组件,与其它DOM元素互不干扰。跟 iframe 相似,是一个独立是沙盒,但它没有自己的 window,有一个轻量级 document,另外 shadowRoot 对象不支持所有的 DOM API ,支持主流的 getElementById、querySelector 和 querySelectorAll 等方法


结构:
Element.attachShadow() 方法会将 shadow DOM 树附加给特定元素,并返回它的 ShadowRoot。该方法只有一个对象类型,一个 Key 值 mode,可以设置为 open 或 closed 来指定该模式的打开和关闭。open 状态表示可以通过 JavaScript 来获取 Shadow DOM,close 状态 shadowRoot 将会返回 null。
javascript
1let shadow = elementRef.attachShadow({mode: 'open'}); 2let myShadowDom = myCustomElem.shadowRoot;
例子:
javascript
1window.customElements.define('user-card', UserCard); 2class FooBar extends HTMLElement { 3 constructor() { 4 super(); 5 this.attachShadow({ mode: 'open' }); 6 this.innerHTML = 'foo-bar'; 7 } 8 connectedCallback() { 9 this.shadowRoot.innerHTML = ` 10 <p>I'm in the Shadow Root!</p> 11 `; 12 } 13} 14window.customElements.define('foo-bar', FooBar); 15
并非所有 HTML 元素都可以开启 Shadow DOM,例如用 img 这样的非容器素作为 Shadow Host 不合理,而且会报错。目前支持的元素: article、 body、h1 ~ h6、header、 p、 aside、 div、aside、nav、span、section、main、footer、blockquote。
javascript
1document.createElement('img').attachShadow({mode: 'open'}); 2// => DOMException
另一个标准 HTML Imports (例如使用 <link rel="import" href="myfile.html >),已废弃不再详述。
2016 年 Safari 开始支持 Custom Elements 和 Shadow Dom,Firefox 则是在 2017 年跟进,目前各 API 兼容性如下:




css
1<style> 2 user-card { 3 border: 1px solid red; 4 } 5</style> 6
与目前其它框架的比较
组件传值监听和事件绑定:
javascript
1class CustomComponent extends HTMLElement { 2 static get observedAttributes() { 3 return ["attributesName"]; 4 } attributeChangedCallback(name, oldValue, newValue) { 5 // 当属性值变更时做一些操作 6 } 7}
javascript
1class Button extends HTMLElement { 2 this.$btn = this._shadowRoot.querySelector('button'); 3 this.$btn.addEventListener('click', () => {} 4}
javascript
1class ClickCounter extends HTMLElement { 2 constructor() { 3 super(); 4 5 this._timesClicked = 0; 6 7 var button = document.createElement("button"); 8 button.textContent = "Click me"; 9 button.onclick = (evt) => { 10 this._timesClicked++; 11 this.dispatchEvent(new CustomEvent("clicked", { 12 detail: this._timesClicked 13 })); 14 }; 15 16 this.append(button); 17 } 18}; 19customElements.define("click-counter", ClickCounter); 20var counter = document.querySelector("click-counter"); 21counter.addEventListener("clicked", (evt) => { 22 console.log(evt.detail); 23}); 24