构建自己的web components

前面先吹一下牛逼

“自定义组件”,“组件化”渐渐成了前端编程的日常,我们把具有通用功能的东西提取出来,然后封装一个“组件”,方便重复使用,而不用再重复写这段逻辑以及样式。

只是问题是,每一个框架都有自己的一套规则写法,用 react 编写一个组件,无法在 vueangular里边使用,反之亦然。这多么的让人不爽,如果我之前在使用 react 的一个项目里边写了一个组件,然后到了一个 vue 的项目里边遇到类似的组件,不能直接套用,而是要按照 vue 的规则来重写一个?(当然,事实上,一般一些常用的组件,每个框架都会有对应的写好的组件)

当然不是,我们今天介绍一下使用web components相关的技术来构建一个通用的组件库吧!

完整代码在这里!

web components

前边提到的一些“组件化”,都是指的是受困于某一个框架技术下的组件,要使用这个封装好的组件,必须要依附于某一个框架环境之下才能正常的运作,一旦离开了这个框架,这个组件就不再可用了。而web components则是W3C制定的一系列关于原生自定义组件的方法规则,因为它是官方指定的,所以在大部分的现代浏览器下,其大部分的属性跟方法都得到了支持。而跟不上节奏的浏览器请参考IE跟Edge…

其实web components很早就已经被提出来了,只是在国内并没有流行起来,可能很大一部分原因要归结于国内的浏览器环境吧,前几年以前,国内IE内核的浏览器还是挺多的,而Youtube则已经使用 polymer (一个使用web components技术编写的库)重写了前端界面了。而2018年中旬,腾讯推出 Omi 框架,2018年末,Edge终于放弃了自己追逐标准的梦想,改用chromium内核,这一切,都似乎让web components在国内发热有了一丝希望?

在这里,我并不打算多讲技术的细节,毕竟文档里边写的很清楚,如果大家感兴趣的话,关于技术细节的部分,点击这里去沉浸到技术的海洋去吧!

下边,我将定义一个名为 my-button 的具有 type 属性的组件,该组件会根据 type 属性的值,呈现不同的样式。

基础定义版本

先利用 Custom element以及 ShadowDOM 编写,其实web components比较重要的两个技术要点就是 customElements.define() 以及 shadow domshadow dom带给我们的主要好处是,它为我们的组件创建了一个密封的空间,里边的html,css样式是完完全全隔离的,不会影响到外界。而这也是一些框架,css库在这方面所有的努力,不同的是,shadow dom是天然具备这种的条件的。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class MyButtonCreate extends HTMLElement {
constructor() {
super();

// 构建html结构
let shadow = this.attachShadow({mode: 'open'});

let style = document.createElement('style');
style.textContent = `
:host[hidden] {
display: none;
}
:host {
display: inline-block;
}
button {
padding: 8px;
border-radius: 4px;
color: black;
outline: none;
border: none;
font-size: 16px;
cursor: pointer;
}
.primary-type {
background-color: blue;
color: white;
}
.danger-type {
background-color: red;
color: white;
}
.success-type {
background-color: green;
color: white;
}
`;

let button = document.createElement('button');
let slot = document.createElement('slot');

button.appendChild(slot);
shadow.appendChild(style);
shadow.appendChild(button);
}

// 映射properties与attributes
get type() {
return this.getAttribute('type');
}

set type(newVal) {
this.setAttribute('type', newVal);
}

// 监测attributes变化
static get observedAttributes() {
return ['type'];
}

// attributes变化回调函数
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'type') {
updateStyle(this, newValue);
}
}
}

function updateStyle (elem, newType) {
let shadow = elem.shadowRoot;
let btn = shadow.querySelector('button');
btn.classList.remove('primary-type');
btn.classList.remove('danger-type');
if (newType) {
btn.classList.add(`${newType}-type`);
}
}

customElements.define('my-button-create', MyButtonCreate);

我们可以感受到,这里几乎就是纯js操作dom,写起来并不是特殊友好舒服,为此标准给我们提供了一个稍微友好一点的写法。

模板定义版

模板定义写起来其实跟前边的很像,只是在构建html模板的时候,使用template标签来编写html模板,会更舒服一些:

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 MyButtonTemplate extends HTMLElement {
constructor() {
super();

// 获取index.html里边的template模板,挂载到shadow dom下
let template = document.getElementById('my-button-template').content;
this.attachShadow({mode: 'open'}).appendChild(template.cloneNode(true));
}

// properties与attributes映射
get type() {
return this.getAttribute('type');
}

set type(newVal) {
this.setAttribute('type', newVal);
}

// 监测attributes变化
static get observedAttributes() {
return ['type'];
}

// attributes变化回调函数
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'type') {
updateStyle(this, newValue);
}
}
}

function updateStyle (elem, newType) {
let shadow = elem.shadowRoot;
let btn = shadow.querySelector('button');
btn.classList.remove('primary-type');
btn.classList.remove('danger-type');
if (newType) {
btn.classList.add(`${newType}-type`);
}
}

customElements.define('my-button-template', MyButtonTemplate);

使用lit-element

其实只要细想一下,标准之下给的方法其实都不是很适合完成一些复杂类型的web components的构建。在条件渲染,列表渲染方面的表现其实还是差强人意。我在这里推荐的是伴随着`polymer@3.x推出的lit-element,它本身就是polymer里边的核心代码,去掉了polymer的内置web components(像DomIf,DomRepeat),让它变得更小巧,当然,如果你更喜欢polymer的语法,而且不在乎这点容量问题,我个人其实是更推荐polymer的。不过,除了lit-elementpolymer`,还有很多编写 web components 的库,个人感觉或许都会在写一个复杂的组件的时候给与一定程序的编写帮助。

下边,上代码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { LitElement, html } from 'lit-element';

class MyButtonLit extends LitElement {
// 设置properties,以及对attributes的映射关系
static get properties() {
return {
type: { type: String, reflect: true },
};
}

constructor() {
super();
// 初始化
this._btnClass = '';
}

// attributes变化回调函数
attributeChangedCallback(name, oldval, newval) {
if (name === 'type') this._btnClass = `${newval}-type`;
this.dispatchEvent(new CustomEvent('attr-changed', {
detail: 'test',
bubbles: true
}));
super.attributeChangedCallback(name, oldval, newval);
}

// 构建html结构
render() {
return html`
<style>
:host[hidden] {
display: none;
}
:host {
display: inline-block;
}
button {
padding: 8px;
border-radius: 4px;
color: black;
outline: none;
border: none;
font-size: 16px;
cursor: pointer;
background-color: gray;
color: white;
}
.primary-type {
background-color: blue;
}
.danger-type {
background-color: red;
}
.success-type {
background-color: green;
}
</style>

<button class="${this._btnClass}">
${this.type === 'primary' ?
html`<span>primary</span>`:
html`<span>others</span>`}
<slot></slot>
</button>
`;
}
}

customElements.define('my-button-lit', MyButtonLit);

总体上来讲,lit-element 是在自定义properties以及构建html模板上做了一个封装,提供了更方便的定义 properties 的方式,以及properties映射 attributes的方式,而html模板则让我们以字符串模板的形式来编写,能更好编写,以及封装我们的组件,具有更好的灵活性。

使用写好的web-components

因为上边编写好的组件,都是符合现代浏览器标准的,所以在现代浏览器上,我们都可以非常方便的直接将他们导入,然后使用就可以了。当然,在一些脱节奏的浏览器上(例如IE11,Edge),我们可以通过加入 polyfill 来正常使用我们编写好的组件。

1
2
3
<my-button-create type="primary">create</my-button-create>
<my-button-template type="danger">template</my-button-template>
<my-button-lit type="success">Lit element</my-button-lit>

项目经验

可能在这个react,vue,angular等框架横行的时代,对于新生的web components多多少少会存有疑惑以及担忧,这货真能构建大型项目吗?其实这个倒是真的不用担心,毕竟大名鼎鼎的YouTube就是用web components构建的。而且,就算不用web components构建大型项目,在组件库的构建上边,它肯定能给你带来以前无法带来愉悦。

那么问题来了,jQuery不是也有很多组件库吗?(手动滑稽)

总结

其实,web components还存在着许多的不足之处,进步空间还非常的大,但是它确实是迈出了重要的一步,我觉得它肯定是会在一定程度上影响着前端技术的发展方向。还有就是,标准总是在一些革命性的库/框架的影响下做出改进更新,这对双方都是好事,而最终受益的,始终都是我们开发者。所以,我觉得多多少少可以拥抱一下web components了,毕竟这都9102年了。