WebComponents入门

WebComponents是一项在网页里面直接进行组件化开发的标准,帮助你在不使用任何框架的前提下,进行自定义组件开发。

开发流程

要开发一个自定义组件,非常简单,尤其在你使用过vue等框架,对组件化概念有一定认识之后:

  1. 创建一个类或函数来定义web组件
  2. 使用CustomElementRegistry.define()这个api来注册第一步定义的组件
  3. 组件内部可使用ShadowDom技术来实现内部细节
  4. 通过templateslot元素,在组件内部与外部之间构建灵活的内容替换方式
  5. 在页面其它位置,像普通html元素一样使用前面开发好的自定义组件。

创建自定义组件的类

有两种方式创建自定义组件的类:

  1. 创建一个独立的自定义组件类

示例:

1
2
3
4
5
6
7
8
class PopUpInfo extends HTMLElement {
constructor() {
// 必须首先调用 super方法
super();

// 其它构造写在这里
}
}

这个方式中,通过继承HTMLElement得到一个PopUpInfo类,这个类代表了一个新的自定义组件。 HTMLElement接口,是所有html元素都间接或直接继承的接口。 所以直接继承这个类的方式,得到的自定义组件,是一个完全独立的新组件,可以把它当成一个新的html元素来看待。

  1. 基于已有的标准html元素继承

示例:

1
2
3
4
5
6
7
8
class WordCount extends HTMLParagraphElement {
constructor() {
// 必须首先调用 super方法
super();

// 其它构造写在这里
}
}

这个方式中,通过继承HTMLParagraphElement得到一个WordCount类,HTMLParagraphElement从字面含义可知,它代表的是p元素。 WordCount类继承了p元素之后,将具备p元素的自身的特性。 这样得到的一个自定义组件,在注册和使用时,与前面一种方式得到的自定义组件,是有区别的。

前面总结的两种方式都有共同点:必须有明确的extend目标,必须有constructor函数,必须调用super()函数。

注册和使用自定义组件

使用CustomElementRegistry.define()这个api来注册自定义组件,也有两种方式。通过window.customElements可以得到一个CustomElementRegistry的实例。

  1. 注册独立使用的自定义组件
1
2
3
4
5
6
7
8
customElements.define('popup-info', class PopUpInfo extends HTMLElement {
constructor() {
// 必须首先调用 super方法
super();

// 其它构造写在这里
}
});

以上把PopUpInfo注册为了一个自定义组件,并且可以在html中通过popup-info标签来直接使用。如:

1
<popup-info>

document.createElement中,也可以直接使用:

1
document.createElement('popup-info')

  1. 注册继承了标准元素的自定义组件
1
2
3
4
5
6
7
8
customElements.define('expanding-list', class ExpandingList extends HTMLUListElement {
constructor() {
// 必须首先调用 super方法
super();

// 其它构造逻辑写在这里
}
}, { extends: "ul" });

这类组件不能直接用注册的标签名称使用,而是要基于它扩展的标准元素、结合is这个html属性来使用。如:

1
2
<ul is="expanding-list">
</ul>

document.createElement中,也要做调整,使用isoption:

1
document.createElement('ul', {is: 'expanding-list'})

两种组件注册时的共同要求:标签名称必须是用短横线分割的多个单词,且必须符合DOMString标准。

使用ShadowDom

在自定义组件的内部,通过ShadowDom来构造内部的细节。 从编程的角度来说,组件要与外部进行职责分离,内部细节应该对外不可见;从html元素的角度来说,元素最终都是通过dom结构来展现的。所以ShadowDom就出现了。首先它就是一份普通的dom结构,但与我们常规看到的dom不一样的是,它在它所依附的元素外部,是不可见的。而且,它不是一个新技术概念,浏览器在过去,就已经在使用ShadowDom来封装内部标准元素。比如说video元素,我们在页面中使用的时候,它都自带控制栏,这个控制栏就是ShadowDom完成的。

ShadowDom通过附加在某个常规的dom节点上,间接地参与了整个页面的渲染,这个被附加的dom节点称为ShadowHostShadowDom内部就跟普通dom没有区别,它也有跟document一样的跟节点,称为ShadowRoot。我们把document结构称为document treeShadowDom结构称为ShadowTree。 这是它们的关系:

使用实践

通过Element.attachShadow(),可以给任意元素附加一个ShadowDom

1
2
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});

这个方法调用后偶返回了ShadowDomShadowRoot引用。attachShow使用时,有一个mode的option,它可以配置为:closedopen。当它是open的时候,回头这个附加了ShadowDom的元素,可以通过下面的方式访问ShadowDom

1
elementRef.shadowRoot

closedoption则不可以。所以closedoption特别适合用在自定义组件里面。

当你拿到ShadowRoot之后,就可以往ShadowDom里面,添加普通的DOM节点了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class PopUpInfo extends HTMLElement {
constructor() {
super();

// 附件shadowdom
const shadow = this.attachShadow({mode: 'open'});

// 创建常规dom节点
const wrapper = document.createElement('span');
wrapper.setAttribute('class', 'wrapper');
shadow.appendChild(wrapper);
}
}

自定义元素可以通过htmlattribute往组件内部传递外部的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class PopUpInfo extends HTMLElement {
constructor() {
super();

// 附件shadowdom
const shadow = this.attachShadow({mode: 'open'});

// 创建常规dom节点
const wrapper = document.createElement('span');
wrapper.setAttribute('class', 'wrapper');

const info = document.createElement('span');
// 获取自定义元素在页面中注册的属性
const text = this.getAttribute('data-text');
info.textContent = text;

wrapper.appendChild(info);
shadow.appendChild(wrapper);
}
}

1
<popup-info data-text="...">

还可以在ShadowDom里面编写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
class PopUpInfo extends HTMLElement {
constructor() {
super();

// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});

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;
bottom: 20px;
left: 10px;
z-index: 3;
}

img {
width: 1.2rem;
}

.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`

shadow.appendChild(style);
}
}

总之,在ShadowDom里面能做的事情,与在document中是没啥区别的,它们就是两个完全隔离的dom渲染区域。

使用template和slot

不同于上面直接在js中写内部的html结构和样式,自定义组件还能利用templateslot这个元素来实现更加强大的html结构构建功能。可参考:使用template和slot

使用生命周期函数

在自定义组件中还能使用生命周期函数,来开发更强的组件,可参考: 使用生命周期函数

实例

mdn上有提供诸多实例,包含了web component相关的各个要点,在真正想去实践web component时,可以拿来研究参考:mdn实例