前言
不知不觉,2019年即将接近尾声,现有前端三大框架也各自建立着自己的生态、自己的使用群体。从angular1.0跨时代的开创了前端MVVM模型(在其他平台已经存在的模型,如WPF),到React组件化设计思路的诞生,到Vue借鉴两位前辈的思路,创造属于自己的技术体系。
随着各大框架版本的更迭,组件化的思路因为大大提高了开发效率依旧一直是各大框架的核心(angular从angular2开始),从未改变。其实,早在react诞生之前,组件化这个概念,已经在2011年前端开发者大会上被提出并完成纳入w3c标准。到现在,基本主流的浏览器都对他进行了兼容。本文便是对这一技术的初探,大家写腻了三大框架,不妨看看原生的组件要怎么玩
WebComponent中的三个概念
在WebComponents技术体系中,主要由以下三项技术所组成,通过组合这三项技术,可以创建属于自己功能的组件。
- Custom elements(自定义元素) 用于定义自定义标签。
- Shadow DOM(影子DOM) 类似于沙盒,将dom结构附加到元素上,保证功能或者样式的私有,而不用担心污染其他功能或者样式。
- HTML templates(HTML模板) 可以当做缺少了数据绑定的vue的template标签,主要承担了组件结点渲染,也提供了slot插入内容。
基于以上的内容简介,我们来看看这三项技术具体要怎么使用
Custom elements
Cumtom elements 这个概念对于写惯了三大框架的开发者而言非常的用于理解,自定义标签,我们在其他框架经常通过组件的形式,使用自己定义的标签,就拿vue来举例,我们在vue中会见到下面这样的代码。
<template>
<x-toast>测试</x-toast>
</template>
<script>
import Toast from 'Toast';
export default {
components:{
'x-toast': Toast
}
// ...省略其他代码
}
</script>
在这里,"x-toast"
就是一个自定义标签,用于定义自己的功能,对于Web Component,我们可以使用CustomElementRegistry.define
方法来自定义元素,该方法接受三个参数
- 表示所创建的元素名称的符合DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线。
- 用于定义元素行为的 类
- 一个包含 extends属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
基于以上的定义,我们可以这样定义一个这样的标签。
CustomElementRegistry.define('todo-list', TodoList);
针对第二个参数TodoList
,我们参照参数描述,主要用于定义元素行为的类,他拥有两种类型,通过继承来确定类型方式
- **Autonomous custom elements , 独立元素,即html中可以直接使定义的标签,需要继承 ** HTMLElement
class TodoList extend HTMLElement {
construct(){
super();
}
}
CustomElementRegistry.define('todo-list', TodoList);
在html中我们就可以直接这么使用
<todo-list><todo-list/>
-
Customized built-in elements 继承自基本元素,并不像独立元素一样,他依赖于
div,p
等基本元素标签,通过继承对应的标签,来拓展其功能,具体使用的时候,通过is
属性来区分原生标签。
class TodoList extend HTMLParagraphElement {
construct(){
super();
}
}
CustomElementRegistry.define('todo-list', TodoList, {extends: 'p'});
在html中需要配合is属性使用
<p is="todo-list">
在这里我们提到了如何定义一个元素(组件),对应vue/react组件,WebComponents也有属于自己的生命周期钩子函数,当我们定义一个元素时,他会在元素的不同阶段触发他们。
- connectedCallback:当元素
首次被插入
文档DOM时,被调用。 - disconnectedCallback:当元素从文档DOM中
删除
时,被调用。 - adoptedCallback:当元素被
移动到新的文档
时,被调用。 - attributeChangedCallback: 当元素增加、删除、修改自身属性时,被调用。
在这4个钩子函数中 1、2、4非常好理解,我们都可以从其他框架找到对应,第3可能就比较难于理解,什么叫移动到新的文档时被调用,咱们通过一个例子来说明
function createWindow(srcdoc) {
let p = new Promise(resolve => {
let f = document.createElement('iframe');
f.srcdoc = srcdoc || '';
f.onload = e => {
resolve(f.contentWindow);
};
document.body.appendChild(f);
});
return p;
}
// 1. 创建2个Iframe w1,和w2
Promise.all([createWindow(), createWindow()])
.then(([w1, w2]) => {
// 2. 在w1这个iframe中创建了一个自定义元素'x-adopt'
w1.customElements.define('x-adopt', class extends w1.HTMLElement {
adoptedCallback() {
console.log('Adopted!');
}
});
// 3. 实例化这个自定义元素
let a = w1.document.createElement('x-adopt');
// 4. 将这个自定义元素插入w2这个iframe中
w2.document.body.appendChild(a);
});
上面这个例子,便是移动到新的文档中,我在iframe1中创建了一个属于iframe1的新的元素,但是却将他插入iframe2,这样就是将的其插入其他文档,因此会触发adoptedCallback
生命周期钩子。
注意在WebComponents的attributeChangedCallback
,这个生命周期钩子之中,我们要通过定义observedAttributes
这个静态方法,约定你要监听的属性,才会触发attributeChangedCallback
回调,如下所示
class CustomInput extends Base{
// 定义监听属性
static get observedAttributes() {
return ['value'];
}
// 当自定义元素的一个属性被增加、移除或更改时被调用。:
attributeChangedCallback(name, oldValue, newValue) {
}
}
customElements.define('custom-input', CustomInput);
如上面代码所示,当我改变了custom-input
的value
属性时,才会触发attributeChangedCallback,回调,如果你改变了name
或者其他非value
属性的时候,便不会触发(第一次写Web Components时可能需要注意,第一次本人就怎么也没有办法触发这个回调)
Shadow DOM
Shadow DOM其实并不是一个新的概念,很早之前,Chrome就可以通过控制台Setting
显示页面的Shadow DOM
把这一项勾选后,你在通过控制台Elements看看元素,你会发现有一些原生的标签,也有属于自己的Shadow DOM, 如截图中的
input
元素。#shadow-root
称为起始根节点 ,在图中可以看到,他是寄宿在input
标签之上,当然这是一个最简单的Shadow DOM,其实 Shadow DOM也和普通元素一样,可以嵌套使用,可以在一个#shadow-root
中嵌入别的#shadow-root
。如原生标签video
就是如此,大家可以自己打开控制台看看。
介绍了这么多Shadow DOM的知识,他的主要功能如上面介绍的,他主要保证功能或者样式的私有,而不用担心污染其他功能或者样式
。
那么我们来看看他是怎么和Web Components结合使用
对HTML元素而言,他的实例中有一个方法 attachShadow
,他会返回shadowRoot并挂载到这个元素实例上,因此我们只需要调用他,便可以生成Shadow DOM;
class TodoList extend HTMLElement {
construct(){
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
}
}
CustomElementRegistry.define('todo-list', TodoList);
我们看见,在调用方法时传入了一个对象,对象中有这样的属性{ mode: 'open' }
,当mode传入open
时,你可以通过元素实体this.shadowRoot
获取shadowDOM节点,当传入close
时,便没法这样获取,如video
标签,你无法通过this.shadowRoot
获取到,学过JAVA等面向对象的同学应该会发现,这是不是和将属性定义为private
以及public
很像呢?
template
template是这三个概念之中最简单的了,即使用<template>
标签,来完成shadowRoot结点渲染,因为<template>
标签并不会渲染到html元素上,因此我们可以利用这一特性来复用template。如以下的代码
<template id="my-paragraph">
<style>
:host{
}
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
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));
}
})
我们通过getElementById
获取到模板,然后拿到他的内容,通过拿到的shadowRoot
元素appendChild到结点中去,是不是非常的简单?
我们注意到模板中有一段style
写了一个伪类 :host
,这个伪类主要是给其宿主元素
添加样式。我们可以使用类似这样的选择器,控制不同class下,结点的样式。
如以下样式只在自定义元素存在test
这个类才会生效,如<x-test class="test"></x-foo>
:host(.test:host) {
...
}
一点优化
相对于传统的template
标签,接触webpack后我更喜欢通过模块化的方式引入,对webpack
而言,一切皆是模块,我们只需要写html文件,通过对应loader进来后即可,如:
// template.html
<style>
:host{
}
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
import template from 'template.html';
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'});
shadowRoot.innerHTML = template; // 通过Import 是不是更方便了呢?
}
})
总结
Web Components作为原生的组件化方案,没有数据绑定写起来还是挺麻烦的,不过,对于一些小工具而言,天然不需要任何依赖,项目纯净,写起来也是不错的,最近我也在使用Web Component写一个chrome插件,用于YAPI Mock拦截,也欢迎大家体验使用。
https://github.com/JackyTianer/yapi-mock-chrome-plugin
写文不易,如果觉得文章有用,动动手点个赞吧,您的赞是我创作的最大动力!谢谢