前言
不知不覺牲芋,2019年即將接近尾聲苹支,現(xiàn)有前端三大框架也各自建立著自己的生態(tài)膝蜈、自己的使用群體。從angular1.0跨時代的開創(chuàng)了前端MVVM模型(在其他平臺已經(jīng)存在的模型级历,如WPF),到React組件化設(shè)計思路的誕生叭披,到Vue借鑒兩位前輩的思路寥殖,創(chuàng)造屬于自己的技術(shù)體系。
隨著各大框架版本的更迭涩蜘,組件化的思路因?yàn)榇蟠筇岣吡碎_發(fā)效率依舊一直是各大框架的核心(angular從angular2開始)嚼贡,從未改變。其實(shí)同诫,早在react誕生之前粤策,組件化這個概念,已經(jīng)在2011年前端開發(fā)者大會上被提出并完成納入w3c標(biāo)準(zhǔn)误窖。到現(xiàn)在叮盘,基本主流的瀏覽器都對他進(jìn)行了兼容秩贰。本文便是對這一技術(shù)的初探,大家寫膩了三大框架柔吼,不妨看看原生的組件要怎么玩
WebComponent中的三個概念
在WebComponents技術(shù)體系中萍膛,主要由以下三項技術(shù)所組成,通過組合這三項技術(shù),可以創(chuàng)建屬于自己功能的組件嚷堡。
- Custom elements(自定義元素) 用于定義自定義標(biāo)簽蝗罗。
- Shadow DOM(影子DOM) 類似于沙盒,將dom結(jié)構(gòu)附加到元素上蝌戒,保證功能或者樣式的私有串塑,而不用擔(dān)心污染其他功能或者樣式。
- HTML templates(HTML模板) 可以當(dāng)做缺少了數(shù)據(jù)綁定的vue的template標(biāo)簽北苟,主要承擔(dān)了組件結(jié)點(diǎn)渲染桩匪,也提供了slot插入內(nèi)容。
基于以上的內(nèi)容簡介友鼻,我們來看看這三項技術(shù)具體要怎么使用
Custom elements
Cumtom elements 這個概念對于寫慣了三大框架的開發(fā)者而言非常的用于理解傻昙,自定義標(biāo)簽,我們在其他框架經(jīng)常通過組件的形式彩扔,使用自己定義的標(biāo)簽妆档,就拿vue來舉例,我們在vue中會見到下面這樣的代碼虫碉。
<template>
<x-toast>測試</x-toast>
</template>
<script>
import Toast from 'Toast';
export default {
components:{
'x-toast': Toast
}
// ...省略其他代碼
}
</script>
在這里贾惦,"x-toast"
就是一個自定義標(biāo)簽,用于定義自己的功能敦捧,對于Web Component须板,我們可以使用CustomElementRegistry.define
方法來自定義元素,該方法接受三個參數(shù)
- 表示所創(chuàng)建的元素名稱的符合DOMString 標(biāo)準(zhǔn)的字符串兢卵。注意习瑰,custom element 的名稱不能是單個單詞,且其中必須要有短橫線秽荤。
- 用于定義元素行為的 類
- 一個包含 extends屬性的配置對象甜奄,是可選參數(shù)。它指定了所創(chuàng)建的元素繼承自哪個內(nèi)置元素王滤,可以繼承任何內(nèi)置元素贺嫂。
基于以上的定義,我們可以這樣定義一個這樣的標(biāo)簽雁乡。
CustomElementRegistry.define('todo-list', TodoList);
針對第二個參數(shù)TodoList
,我們參照參數(shù)描述第喳,主要用于定義元素行為的類,他擁有兩種類型踱稍,通過繼承來確定類型方式
- **Autonomous custom elements , 獨(dú)立元素曲饱,即html中可以直接使定義的標(biāo)簽悠抹,需要繼承 ** HTMLElement
class TodoList extend HTMLElement {
construct(){
super();
}
}
CustomElementRegistry.define('todo-list', TodoList);
在html中我們就可以直接這么使用
<todo-list><todo-list/>
-
Customized built-in elements 繼承自基本元素,并不像獨(dú)立元素一樣扩淀,他依賴于
div,p
等基本元素標(biāo)簽楔敌,通過繼承對應(yīng)的標(biāo)簽,來拓展其功能驻谆,具體使用的時候卵凑,通過is
屬性來區(qū)分原生標(biāo)簽。
class TodoList extend HTMLParagraphElement {
construct(){
super();
}
}
CustomElementRegistry.define('todo-list', TodoList, {extends: 'p'});
在html中需要配合is屬性使用
<p is="todo-list">
在這里我們提到了如何定義一個元素(組件)胜臊,對應(yīng)vue/react組件勺卢,WebComponents也有屬于自己的生命周期鉤子函數(shù),當(dāng)我們定義一個元素時象对,他會在元素的不同階段觸發(fā)他們黑忱。
- connectedCallback:當(dāng)元素
首次被插入
文檔DOM時,被調(diào)用勒魔。 - disconnectedCallback:當(dāng)元素從文檔DOM中
刪除
時甫煞,被調(diào)用。 - adoptedCallback:當(dāng)元素被
移動到新的文檔
時冠绢,被調(diào)用抚吠。 - attributeChangedCallback: 當(dāng)元素增加、刪除唐全、修改自身屬性時埃跷,被調(diào)用蕊玷。
在這4個鉤子函數(shù)中 1邮利、2、4非常好理解垃帅,我們都可以從其他框架找到對應(yīng)延届,第3可能就比較難于理解,什么叫移動到新的文檔時被調(diào)用贸诚,咱們通過一個例子來說明
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. 創(chuàng)建2個Iframe w1,和w2
Promise.all([createWindow(), createWindow()])
.then(([w1, w2]) => {
// 2. 在w1這個iframe中創(chuàng)建了一個自定義元素'x-adopt'
w1.customElements.define('x-adopt', class extends w1.HTMLElement {
adoptedCallback() {
console.log('Adopted!');
}
});
// 3. 實(shí)例化這個自定義元素
let a = w1.document.createElement('x-adopt');
// 4. 將這個自定義元素插入w2這個iframe中
w2.document.body.appendChild(a);
});
上面這個例子方庭,便是移動到新的文檔中,我在iframe1中創(chuàng)建了一個屬于iframe1的新的元素酱固,但是卻將他插入iframe2,這樣就是將的其插入其他文檔械念,因此會觸發(fā)adoptedCallback
生命周期鉤子。
注意在WebComponents的attributeChangedCallback
运悲,這個生命周期鉤子之中龄减,我們要通過定義observedAttributes
這個靜態(tài)方法,約定你要監(jiān)聽的屬性班眯,才會觸發(fā)attributeChangedCallback
回調(diào)希停,如下所示
class CustomInput extends Base{
// 定義監(jiān)聽屬性
static get observedAttributes() {
return ['value'];
}
// 當(dāng)自定義元素的一個屬性被增加烁巫、移除或更改時被調(diào)用。:
attributeChangedCallback(name, oldValue, newValue) {
}
}
customElements.define('custom-input', CustomInput);
如上面代碼所示宠能,當(dāng)我改變了custom-input
的value
屬性時亚隙,才會觸發(fā)attributeChangedCallback,回調(diào)违崇,如果你改變了name
或者其他非value
屬性的時候阿弃,便不會觸發(fā)(第一次寫Web Components時可能需要注意,第一次本人就怎么也沒有辦法觸發(fā)這個回調(diào))
Shadow DOM
Shadow DOM其實(shí)并不是一個新的概念羞延,很早之前恤浪,Chrome就可以通過控制臺Setting
顯示頁面的Shadow DOM
把這一項勾選后,你在通過控制臺Elements看看元素肴楷,你會發(fā)現(xiàn)有一些原生的標(biāo)簽水由,也有屬于自己的Shadow DOM, 如截圖中的
input
元素。#shadow-root
稱為起始根節(jié)點(diǎn) 赛蔫,在圖中可以看到砂客,他是寄宿在input
標(biāo)簽之上,當(dāng)然這是一個最簡單的Shadow DOM,其實(shí) Shadow DOM也和普通元素一樣呵恢,可以嵌套使用鞠值,可以在一個#shadow-root
中嵌入別的#shadow-root
。如原生標(biāo)簽video
就是如此渗钉,大家可以自己打開控制臺看看彤恶。
介紹了這么多Shadow DOM的知識,他的主要功能如上面介紹的鳄橘,他主要保證功能或者樣式的私有声离,而不用擔(dān)心污染其他功能或者樣式
。
那么我們來看看他是怎么和Web Components結(jié)合使用
對HTML元素而言瘫怜,他的實(shí)例中有一個方法 attachShadow
,他會返回shadowRoot并掛載到這個元素實(shí)例上术徊,因此我們只需要調(diào)用他,便可以生成Shadow DOM鲸湃;
class TodoList extend HTMLElement {
construct(){
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
}
}
CustomElementRegistry.define('todo-list', TodoList);
我們看見赠涮,在調(diào)用方法時傳入了一個對象,對象中有這樣的屬性{ mode: 'open' }
暗挑,當(dāng)mode傳入open
時笋除,你可以通過元素實(shí)體this.shadowRoot
獲取shadowDOM節(jié)點(diǎn),當(dāng)傳入close
時炸裆,便沒法這樣獲取垃它,如video
標(biāo)簽,你無法通過this.shadowRoot
獲取到,學(xué)過JAVA等面向?qū)ο蟮耐瑢W(xué)應(yīng)該會發(fā)現(xiàn),這是不是和將屬性定義為private
以及public
很像呢嗤瞎?
template
template是這三個概念之中最簡單的了墙歪,即使用<template>
標(biāo)簽,來完成shadowRoot結(jié)點(diǎn)渲染贝奇,因?yàn)?code><template>標(biāo)簽并不會渲染到html元素上虹菲,因此我們可以利用這一特性來復(fù)用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
獲取到模板掉瞳,然后拿到他的內(nèi)容毕源,通過拿到的shadowRoot
元素appendChild到結(jié)點(diǎn)中去,是不是非常的簡單陕习?
我們注意到模板中有一段style
寫了一個偽類 :host
霎褐,這個偽類主要是給其宿主元素
添加樣式。我們可以使用類似這樣的選擇器该镣,控制不同class下冻璃,結(jié)點(diǎn)的樣式。
如以下樣式只在自定義元素存在test
這個類才會生效损合,如<x-test class="test"></x-foo>
:host(.test:host) {
...
}
一點(diǎn)優(yōu)化
相對于傳統(tǒng)的template
標(biāo)簽省艳,接觸webpack后我更喜歡通過模塊化的方式引入,對webpack
而言嫁审,一切皆是模塊跋炕,我們只需要寫html文件,通過對應(yīng)loader進(jìn)來后即可律适,如:
// 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 是不是更方便了呢?
}
})
總結(jié)
Web Components作為原生的組件化方案捂贿,沒有數(shù)據(jù)綁定寫起來還是挺麻煩的纠修,不過,對于一些小工具而言眷蜓,天然不需要任何依賴分瘾,項目純凈,寫起來也是不錯的吁系,最近我也在使用Web Component寫一個chrome插件,用于YAPI Mock攔截白魂,也歡迎大家體驗(yàn)使用汽纤。
https://github.com/JackyTianer/yapi-mock-chrome-plugin
寫文不易,如果覺得文章有用福荸,動動手點(diǎn)個贊吧蕴坪,您的贊是我創(chuàng)作的最大動力!謝謝