前端組件化其實是個持續了很長時間的過程了,從最開始的jquery的插件開始。我們就在封裝一些js,css,html來方便下次的復用了。基本各個公司都有自己的組件庫,比如我在的點評就是npm的組件。現在組里隨著一些項目往React上轉,React的組件化工作也在不斷的整理中。
組件化的過程中出現了一些普遍的痛點。Web components解決了其中的一些棘手的問題,在這里介紹一下Web Components的一些現狀。
問題
組件化的過程中主要遇到的一些問題:
- 資源分來管理,重用不便
- 缺乏封裝
- 缺乏移植性
首先,js,css和html基本是分開放置的,我們想要重用某塊功能的時候,我們需要在我們的js里面想辦法引入需要的js;然后得根據組件要求的DOM結構引入HTML;然后在我們的css里引入組件需要的css,這其實是個很痛苦的過程。二是缺乏封裝,尤其體現在CSS上,CSS一直都是全局的,沒有局部CSS的概念,雖然可以通過Css Modules來模擬,但是還是有一些限制的,下文也會進行比較。三是組件缺乏移植性,就比如遷到了react的環境。當然我們可以在webpack上做一些文章,讓以前的組件支持在React項目中使用。可是React自有自己的一套狀態管理的機制,很多組件我們還是要重寫的,將來切換到新的環境,遷移是有很大代價的。Web Components能夠解決上面的這些問題
Web Components簡介
Web Components它本身不是一個規范,他是由W3C提出的另外4個規范的合集。這四個規范是:
- Shadow Dom(草案階段)
- Custom Elements(草案階段)
- HTML Template(html5)
- HTML Imports(草案階段)
如上,這四個規范除了template已經成為了HTML5的規范,其他3個還是處于草案階段的,所以瀏覽器的支持情況比較差也是可以理解的了。接下來這四個規范一個個聊一聊。
Shadow Dom
他的作用是:管理多DOM樹的層級關系,更好的合成DOM。他的中心思想是封裝一個完全獨立于文檔流的子DOM樹。他完美的做到了css的封裝
。當然還有文檔內容的封裝。以及通過重定向事件做了事件層面的封裝。當然封裝是他提供的能力,作為使用者的我們其實很關心的是主文檔與Shadow Dom的交互,這個在下面會提到。
創建Shadow Dom
使用的第一步是創建,Shadow Dom的創建得基于一個文檔中已經存在的一個元素,也就是宿主元素。然后通過createShadowRoot方式創建。注意宿主元素的內容是不會被渲染的。
內容的傳遞
我們可以通過content元素來將內容映射到shadow dom中來顯示。還可以通過設置select的屬性來決定那一塊被映射。使用如下:
<!-- 頁面 -->
<div id="nameTagTwo">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
<!-- shadow dom -->
<div style="color: red;">
<content select=".first"></content>
</div>
<div style="color: yellow;">
<content select="div"></content>
</div>
<div style="color: blue;">
<content select=".email"></content>
</div>
樣式的影響
Shadow Dom的樣式被完全封裝,內部的樣式對外部完全沒有影響。文檔流中的樣式也對內部沒有影響。這一點其實很重要。因為只有這樣,才能保證組件的無傷。但是我們使用過程中肯定也會想要有時對shadow dom中的樣式進行一定的改寫的。Shadow Dom提供了這樣的接口。
組件->影響文檔流
組件內部只能使用:host
來改變宿主元素的樣式,頁面的其他內容也是無法影響的
:host(x-foo:host) {
/* 當宿主是 <x-foo> 元素時生效。 */
}
:host(div) { {
/* 當宿主或宿主的祖先元素是
<div> 元素時生效。 */
}
文檔流->組件
文檔流可以通過::shadow或者/deep/來影響組件的樣式。如果想要修整content元素的樣式,使用::content。chrome自己使用了<<和<<<,不過這里就不討論了。
::shadow和/deep/
<style>
#host ::shadow span {
color: red;
}
#host /deep/ span {
color: red;
}
</style>
/*為了與content一起使用的話::content*/
Shadom Dom與CSS modules
Shadow Dom的CSS封裝很好的解決了現在CSS的一個大問題,因為CSS目前還是全局作用域的。所以CSS的模塊化還是通過各個技術hack來實現。現在有兩種:
- 放棄CSS,使用js或json寫樣式。缺點:放棄css處理器;JS語法寫CSS。代表:Radium,jsxstyle,react-style。實現:行內屬性
- CSS編寫,使用JS來管理樣式依賴。缺點:得與webpack綁定使用。代表:CSS Modules,Vuejs。實現:生成獨特的className(文件名—類名—hash值)
上面的兩種是目前的CSS Module的實現方式,實際上都是一種hack。Shadow Dom實際上才是CSS模塊化的完美解決方案。
事件的封裝
Shadow Dom對于事件通過在冒泡階段target的重定向來封裝事件,然后一些可能對頁面造成影響的事件,Shadow Dom就會影藏掉這些事件,也就是在冒泡到主頁面的過程中被擋住了。
就像圖中所示,普通點擊時,target會是我們真正點擊的元素,而Shadow Dom則會將事件的target重定向到宿主元素身上,主要是為了保證組件內部的封裝。多層宿主的時候我也試過了,每層都會重定向到自己的宿主身上。
還有一些事件不會冒泡到主文檔流:abort, error, select, change, load, loadedmetadata, reset, resize,scroll and selectstart。
Shadom Dom與Virtual Dom的比較
Shadow Dom是W3c的規范,它主要被我們用來處理Dom樹之間的關系,他的主要思想是封裝。它本身還是Dom。
Virtual Dom是React的一個選擇,它被用來作為高性能的保證。高性能的地方主要體現在,我們平常操作Dom的時候很多時候刷新操作就是將一塊HTML替換,我們的操作會觸發大量的repaint和reflow,這些操作都是很耗瀏覽器性能的。Virtual Dom將這些操作打包,并且通過一些Diff算法來得出如何通過最簡單的方式改變成我們想要的模樣。它本身是Dom的一層抽象,不是真實的Dom。
Custom Elements
這個的一大好處在于拯救了我們還是很缺乏語義化能力的文檔。雖然HTML5已經在語義化的道路上走的很遠。但是文檔的語義化能力其實還是很薄弱。尤其是現在單頁應用比較多。很多頁面只聲明了一個入口的div。而框架為了更大的兼容性,往往生成的就是一堆簡單的div。
Custom Elements提供了我們自定義元素的能力。當然,語義化并不是關鍵,關鍵是它還提供了我們對語義化元素綁定功能的能力。
Custom Elements--創建
3種方式:
申明 + registerElement
registerElement + new + appendChild
createElement + appendChild
不細講了,看下面的例子,很容易理解。名字要求是必須以連字符連接的,這個在未來還可以當做作用域來限制。
<x-foo class="x-foo"></x-foo>
<script>
document.querySelector('x-foo').addEventListener('click',function(){
alert('文檔中申明出來的');
})
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('直接create出來的');
});
document.body.appendChild(xFoo);
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
var xFoo = new XFoo();
xFoo.addEventListener('click', function(e) {
alert('注冊之后,new出來的');
});
document.body.appendChild(xFoo);
</script>
Custom Elements--綁定功能
其實就是很簡單的通過JS的方式綁定功能。代碼如下:
var XFooProto = Object.create(HTMLElement.prototype);
// 1. 為 x-foo 創建 foo() 方法
XFooProto.foo = function() {
alert('foo() called');
};
// 2. 定義一個只讀的“bar”屬性
Object.defineProperty(XFooProto, "bar", {value: 5});
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
Custom Elements--生命周期
講到現在,我們的功能還一直是調用方在綁定。這一點其實蠻蠢的,我們想要得到的其實是已經綁定了完整功能的組件。需要的是拿來立即能使用的東西。這里Custom Elements提供了一些生命周期讓我們組件可以在初始化的過程中就給自己綁定上方法:
- createdCallback:創建元素實例
- attachedCallback:向文檔插入實例
- detachedCallback:從文檔中移除實例
- attributeChangedCallback(attrName, oldVal, newVal):添加,移除,或修改一個屬性
使用的過程就像下面:
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
console.log("創建時觸發");
this.innerHTML = "<b>我終于有內容了</b>";
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
proto.attachedCallback = function() {
console.log("插入文檔時觸發");
};
proto.attributeChangedCallback = function(attrName,oldVal,newVal) {
console.log(attrName, oldVal, newVal);
};
var XFoo = document.registerElement('x-foo', {prototype: proto});
var xfoo = new XFoo();
xfoo.setAttribute('value',12)
document.body.appendChild(xfoo);
Template
通過上面的Shadow Dom和Custom Elements,其實我們已經實現了組件的自定義以及封裝。不過我們的模板最后一直使用的字符串。最后通過innerHtml的方式插入。包括現在其實JS模版其實全是這么實現的。這樣子的壞處在于當我們多次使用一個模板的時候(比如刷新操作),每次都得把一段字符串轉化為DOM結構,這其實是很費瀏覽器的性能的。
Template允許我們在文檔中申明一段HTML,他在瀏覽器的解析過程中不會有任何的副作用。他里面的元素img,script等等都不會發請求。完全無害。但是他又不是僅僅作為字符串存在。他是被解析成了Document Fragment。這樣每次重用的時候就不會有解析為Dom這種浪費性能的操作。
Html Imports
現在我們需要的就是將這個組件打包出去。其實一直以來關于HTML的引入就一直沒有一個好的解決方案。嵌入iframe的話,頁面之間的交互調用其實很麻煩。用一個script標簽埋在頁面上作為字符串也比較tricky。使用ajax來拉取html的話也會比較奇怪。
除了這個問題,我們一直以來引入資源都是件比較麻煩的事情。比如我們想要引入bootstrap,我們得要手動引入css,再引入js,然后根據bootstrap提供的html結構來使用。
Html Imports就解決了這個問題,他可以用來打包資源
與優雅的引入HTML
。
<!-- 如下 -->
<!-- <link rel="import" href=“/path/to/imports/stuff.html"> -->
使用其實很簡單,就是用link標簽,也可以用js來創建一個link標簽。不過這個是有同域限制的。感覺還是得HTTP2時代,把現在的CDN分域問題解決了才能有使用的價值。
Web Components的兼容性
如圖,chrome算是很激進了,安卓也得是比較新的版本。safari,IE,FF的支持都很差。這里還有一張圖:
這張圖的意思的FF已經把Custom Elements和Shadow Dom立了development flag,將會去實現他。而Html Imports暫時hold on。這個和Safari暫時hold住了Custom Elements和Html Imports的原因一樣。他們都覺得這個和ES6的modules解決的是同一個問題。他們在等待ES6的modules的實施效果。而最新的IE的申明則是,這三個規范都在思考中,應該是都會去實現。
Web Components的polyfill
也就是說這些個規范我們想單純的使用時沒有辦法的。但是他是有組織提供了polyfill的。這個polyfill還是有很大的問題的,IE只能支持到IE11,而且shadow dom的CSS封裝沒有官方的支持也是沒法完美實現的。
相關的框架
他的相關框架有Polymer,X-Tag,SKATEJS,Bosonic,這四個框架大部分都是對他的API的友好封裝。Polymer是google出品,目前也是有15000的star的,比較火,除了封裝了API,他還像Angular一樣做了一層數據的雙向綁定。但是這幾個框架都是使用的上面的Polyfill,所以上面提高的問題他們也都有。
Web Components與React
這里我想比較一下這兩者。因為React官網文檔專門有一篇解釋他們兩者解決的是不同的問題。
Web Components個人感覺是HTML提出的模塊化,他的目的是復用web組件,主要思想是封裝。
React是為了搭建交互式UI,主要是針對不同的狀態顯示不同的View,處理的是view與data同步。
React官網文檔也有實例如何在React中使用Web Components。其實就是在ComponentDidMount的時候初始化一下Web Components,很簡單的使用。
Web Components與React Components
這里我還想比較一下這兩個組件系統,因為他們其實有不少相似點的。
Web Components優點:
- HTML規范
- 復用性,移植性高
- css樣式隔離
- js做js的事情,html做html的事情,css做css的事情(這個有待爭議)
React Components優點:
- virtual dom支持服務器渲染,seo友好(這個也有待爭議,因為google爬蟲是可以跑js的)編寫測試方便(這個也有待爭議)
- 瀏覽器支持情況好,新版本支持到IE9,v0.14支持到IE8
- 抽象做的更好,組件狀態管理
Web components 與 Vuejs
這里還想提一下Vuejs,因為看CSS Modules的時候看到Vuejs自己也實現了CSS的模塊化的,就去看了一下他的文檔。發現他幾乎是實現了一套Web components。他的組件的創建,注冊,繼承,生命周期都和Web components很像。看作者自己與其他框架比較的時候也說了,Vuejs和Polymer的區別就在于Vuejs不依賴于Web components,不需要polyfill。所以想使用Web components的可以去使用Vuejs。他能支持到IE9。使用場景會比較多。
總結
總體來說,Web component他是w3c標準,基本會是組件技術的最終方向,但是需要大量的時間來讓來讓瀏覽器支持。
文章可以直接訪問我的前端網站來查看,平時的日常整理也都會記錄上去。
轉載的話,請注明出處
參考:
- http://www.html5rocks.com/zh/tutorials/webcomponents/shadowdom-201
- http://www.html5rocks.com/zh/tutorials/webcomponents/customelements
- http://webcomponents.org/articles/a-quick-polymer-introduction
- https://facebook.github.io/react/docs/webcomponents.html
- http://benmccormick.org/2014/08/28/custom-elements-by-example
- https://www.w3.org/standards/techs/components#w3c_all
- http://www.csdn.net/article/2015-08-11/2825439-vue