Web Components 是 W3C 在2014年提出的网页组件式开发的技术规范,旨在给 Web 开发者提供浏览器原生级别的组件能力。通过 Web Components 规范开发者可以自定义可重用的 Web 组件并引入到任意项目中而不必借助 Web 组件化框架,并且不必考虑组件内样式和变量污染的问题。
Web Components 相关的 Web 标准
Web Components 主要基于以下几种 Web 标准:
自定义元素 (Custom Elements)
Custom Elements 规范 为设计和使用新类型的 DOM 元素奠定了基础。它允许定义自定义元素及其行为,然后可以在 HTML 中按照需要使用它们。
影子 DOM (Shadow DOM)
Shadow DOM 规范 定义了如何在 Web 组件中使用封装的样式和标记。封装的 Web 组件内的元素可以无法被外部 Javascript 获取,CSS 样式也与Web 组件外部的样式隔离。
HTML 内容模板 (HTML Template)
HTML Template 规范 定义了如何声明标记的 HTML 片段,这些 HTML 片段在页面加载时不会被使用,但可以在运行时实例化。
浏览器支持情况
Chrome | Firefox | Safari | Edge | Opera | |
---|---|---|---|---|---|
Custom Elements | >67 | >63 | >10.1 | >79 | >54 |
Shadow DOM | >53 | >63 | >10 | >79 | >40 |
HTML Template | >90 | >13 | >8 | >90 | >15 |
Class definitions | >49 | >45 | >9 | >13 | >36 |
目前主流浏览器的最新版本都支持这些特性,对于老版本可以使用 WebcomponentsJS polyfill @webcomponents/webcomponentsjs
,但是它没有真正的提供局部CSS。这意味着在不同 Web Components 中如果有同样的 class 和 id ,在同一个 document 中,它们将会发生冲突。此外 Shadow DOM 的 css 选择器 :host()
:sloted()
可能无法正常工作。
如何创建一个 Web Component
创建一个继承自 HTMLElement 的类来指定 Web 组件的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class UserCard extends HTMLElement {
constructor() {
super();
const image = document.createElement('img');
image.src = 'https://mangon.cn/blog/images/favicon.ico';
image.classList.add('image');
const container = document.createElement('div');
container.classList.add('container');
const name = document.createElement('p');
name.classList.add('name');
name.innerText = 'Name';
const email = document.createElement('p');
email.classList.add('email');
email.innerText = 'Email';
const like = document.createElement('button');
like.classList.add('like');
like.innerText = 'Like';
container.append(name, email, like);
this.append(image, container);
}
}使用
customElements.define()
方法注册自定义元素1
window.customElements.define('user-card', UserCard);
customElements.define()
的第一个参数为元素名,根据规范,自定义元素的名称必须包含连词线,用于与原生 HTML 元素进行区别。第二个元素为自定义的继承自 HTMLElement 的类,第三个参数为可选参数对象,目前只有一个extends
参数指定继承于已创建的元素(比如原生 HTML 元素)使用
<template>
可以更方便的定义一个HTML模板,而不是使用复杂的 DOM API。使用<slot>
可以在模板中添加一个可选的占位的插槽,以便与其它的元素组合1
2
3
4
5
6
7
8
9<template id="user-card-template">
<img src="https://mangon.cn/blog/images/avatar.jpeg" class="image">
<div class="container">
<p class="name">Name</p>
<p class="email">Email</p>
<button class="like">Like</button>
<slot name="description"></slot>
</div>
</template>可以在 UserCard 类中更方便的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class UserCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
// 可以为自定义元素添加参数
if (this.hasAttribute('name')) {
const name = this.getAttribute('name');
content.querySelector('.name').innerText = name;
}
if (this.hasAttribute('email')) {
const email = this.getAttribute('email');
content.querySelector('.email').innerText = email;
}
if (this.hasAttribute('img')) {
const img = this.getAttribute('img');
content.querySelector('.image').setAttribute('src', img);
}
// 也可以为自定义元素中的元素绑定事件
content.querySelector('.like').addEventListener('click', (e) => {
window.alert('Thanks!');
});
this.appendChild(content);
}
}使用
Element.attachShadow()
方法将一个 shadow DOM 附加到自定义元素上1
2
3
4
5
6
7
8
9
10
11class UserCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('user-card-template');
const content = template.content.cloneNode(true);
...
this.attachShadow({
mode: 'closed'
}).appendChild(content);
}
}attachShadow()
方法的 mode 参数可以是 open 或者 closed。这定义了 Shadow RooT 的内部实现是否可被 JavaScript 访问及修改 — 也就是说,该实现是否公开。可以为 HTML Template 添加 scoped css 样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53<template id="user-card-template">
<style>
:host {
display: flex;
align-items: center;
width: 450px;
height: 180px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
border-radius: 3px;
overflow: hidden;
padding: 10px;
box-sizing: border-box;
}
.image {
flex: 0 0 auto;
width: 120px;
height: 120px;
vertical-align: middle;
border-radius: 50%;
}
.container {
box-sizing: border-box;
padding: 20px;
height: 160px;
}
.container > .name {
font-size: 20px;
font-weight: 600;
line-height: 1;
margin: 0;
margin-bottom: 5px;
}
.container > .email {
font-size: 12px;
opacity: 0.75;
line-height: 1;
margin: 0;
margin-bottom: 15px;
}
.container > .like {
padding: 8px 24px;
font-size: 12px;
border-radius: 8px;
background: #fff;
border: 1px solid #666;
text-transform: uppercase;
}
.container > .like:hover {
box-shadow: 5px 2px 5px rgba(0, 0, 0, 0.1);
}
</style>
...
</template>:host
表示选择 Shadow DOM 的 Shadow Host ,内容是 Shadow Host 使用的 CSS在页面任何位置使用自定义元素,就像使用常规 HTML 元素那样。
1
2
3<user-card img="https://mangon.cn/blog/images/avatar.jpeg" name="Mangon" email="gaoxiang.like@163.com">
<p slot="description">Front-End Software Engineer</p>
</user-card>
Custom Elements 的生命周期
connetedCallback
当自定义元素添加到 DOM 中时触发
disConnectedCallback
当自定义元素从 DOM 中移除时触发
attributeChangedCallback
当组件的属性改变时触发
adoptedCallback
当自定义元素被移动到新的文档中时触发
Web Components 相关的库
- Ploymer 是一个由 Google 开发的 Web Component 库,目前已不再提供更新支持。
- Lit 是一个由 Google 开发并支持的 Web Components 组件工具库,作为 Ploymer 的替代品。它封装了常用的自定义组件逻辑,支持响应式的属性模板更新绑定。
1 | import {LitElement, css, html} from 'lit'; |
Lit 使用了 Decorator 语法,所以必须使用 Typescript 或者 Babel 的编译器来构建代码。