搞前端時間比較長的同學都會知道一個東西,那就是HTC(HTML Components)
,這個東西名字很現在流行的Web Components
很像,但卻是不同的兩個東西,它們的思路有很多相似點,但是前者已是昨日黃花,後者方興未艾,是什麼造成了它們的這種差距呢?
HTML Components的一些特性
因為主流瀏覽器裡面只有IE
支援過HTC
,所以很多人潛意識都認為它不標準,但其實它也是有標準文件的,而且到現在還有連結,注意它的時間!
http://www.w3.org/TR/NOTE-HTM...
我們來看看它主要能做什麼呢?
它可以以兩種方式被引入到HTML
頁面中,一種是作為“行為”被附加到元素,使用CSS
引入;一種是作為“元件”,擴充套件HTML
的標籤體系。
行為
行為(Behavior
)是在IE5
中引入的一個概念,主要是為了做文件結構和行為的分離,把行為通過類似樣式的方式隔離出去,詳細介紹在這裡可以看:
http://msdn.microsoft.com/en-...
行為裡可以引入HTC
檔案,剛才的HTC
規範裡就有,我們把它摘錄出來,能看得清楚一些:
// engine.htc
<HTML xmlns:PUBLIC="urn:HTMLComponent">
<PUBLIC:EVENT NAME="onResultChange" ID="eventOnResultChange" />
<SCRIPT LANGUAGE="JScript">
function doCalc()
{
:
oEvent = createEventObject();
oEvent.result = sResult;
eventOnResultChange.fire (oEvent);
}
<HTML xmlns:LK="urn:com.microsoft.htc.samples.calc">
<HEAD>
<STYLE>
LK\:CALC { behavior:url(engine.htc); }
</STYLE>
</HEAD>
<LK:CALC ID="myCalc" onResultChange="resultWindow.innerText=window.event.result">
<TABLE>
<TR><DIV ID="resultWindow" STYLE="border: '.025cm solid gray'" ALIGN=RIGHT>0.</DIV></TR>
<TR><TD><INPUT TYPE=BUTTON VALUE=" 7 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 8 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 9 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" / "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" C "></TD>
</TR>
<TR><TD><INPUT TYPE=BUTTON VALUE=" 4 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 5 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 6 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" * "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" % " DISABLED></TD>
</TR>
<TR><TD><INPUT TYPE=BUTTON VALUE=" 1 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 2 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" 3 "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" - "></TD>
<TD><INPUT TYPE=BUTTON VALUE="1/x" DISABLED></TD>
</TR>
<TR><TD><INPUT TYPE=BUTTON VALUE=" 0 "></TD>
<TD><INPUT TYPE=BUTTON VALUE="+/-"></TD>
<TD><INPUT TYPE=BUTTON VALUE=" . "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" + "></TD>
<TD><INPUT TYPE=BUTTON VALUE=" = "></TD>
</TR>
</TABLE>
</LK:CALC>
</HTML>
這是一個計算器的例子,我們先大致看一下程式碼結構,是不是很清晰?再看看現在用jQuery
,我們是怎麼實現這種東西的:是用選擇器選擇這些按鈕,然後新增事件處理函式。注意你多了一步選擇的過程,而且,整個過程混雜了宣告式和命令式兩種程式碼風格。如果按照它這樣,你所有的JS基本都丟在了隔離的不相關的檔案中,整個是一個配置的過程,分離得很乾淨。
除了這種計算器,還有規範文件中舉例的改變介面展示,或者新增動畫之類,注意它們的切入點,都是相當於附加在特定選中元素上的行為,即使DOM
不給JS
暴露任何選擇器,也毫無影響,因為它們直接就通過CSS
的選擇器掛到元素上了。
這種在現在看來,意義不算明顯,現在廣為使用的先選擇元素再新增事件,也是不錯的展現和行為分離方式。
但另外一種使用方式就不同了。
元件
狹義的HTML5
給我們帶來了什麼?是很多新增的元素標籤,比如section,nav,acticle
,那這些東西跟原先直接用div
實現的,好處在哪裡呢?在於語義化。
所謂語義化,就是一個元素能清晰表達自己是幹什麼的,不會讓人有歧義,像div
那種,可以類比成是一個Object
,它不具體表示什麼東西,但可以當成各種東西來用。而nav
一寫,就知道,它是導航,它就像有class
定義的一個實體類,能表達具體含義。
那麼,原有的HTML
元素顯然是不夠的,因為實際開發過程中要表達的東西顯然遠遠超出這些元素,比如日曆,這種東西就沒有一個元素用來描述它,更不用說在一些企業應用中可能會出現的樹之類複雜控制元件了。
不提供原生元素,對開發造成的困擾是程式碼寫起來麻煩,具體可以看之前我在知乎的一個回覆,第三點:
http://www.zhihu.com/question...
所以,大家都想辦法去提供自己的擴充元素的方式,現在我們是知道典型的有angularjs
,polymer
,但很早的時候也不是沒有啊:
http://msdn.microsoft.com/en-...
看,這就是HTC
的新增自定義元素的方式,每個元素可以定義自己對外提供的屬性、方法,還有事件,自己內部可以像寫一個新頁面一樣,專注於實現功能。而且你發現沒有,它考慮得很長遠,提供了名稱空間,防止你在一個頁面引入兩個不同組織提供的同名自定義元素。
這個東西就可以稱為元件了,它跟外界是完全隔離的,外界只要把它拿來就可以用,就像用原生元素一樣,用選擇器選擇,設定屬性,呼叫方法,新增事件處理等等,而且,注意到沒有,它的屬性是帶get
和set
的,這是多麼夢寐以求的東西!
正是因為它這麼好用,所以在那個時代,我們用它幹了很多東西,封裝了各種基礎控制元件,比如樹,資料表格,日期選擇,等等,甚至當時也有人嫌棄瀏覽器原生select
和radio
不好看,用這麼個東西,裡面封裝了圖片來模擬功能,替換原生的來用。
當時也有人,比如我在04年就想過,能不能把這些擴大化,擴充套件到除了基礎控制元件之外的地方,把業務的元件也這麼搞一下,一切皆元件,多好?
但有些事情我直到後來很久以後才想明白,基於業務的端到端元件雖然寫起來很方便,卻是有致命缺陷的。
到這裡為止,對HTML Components
的回顧告一段落,也不討論它為什麼就沒了之類,這裡面爭議太大,我只想談談從這裡面,能看到Web Components
這麼個大家寄予厚望的新標準需要面對一些什麼問題。
Web Components的挑戰
以下逐條列出,挨個說明,有的已經有了,有的差一些,有的沒有,不管這麼多,總之談談我心目中的這個東西應當是怎樣的。
自定義元素標籤支援名稱空間
原因我前面已經說了,可能會有不同組織實現同類功能的元件,存在於同一個頁面內,引起命名歧義,所以我想了很久,還是覺得有字首比較好:
<yours:ComponentA></yours:ComponentA>
<his:ComponentA></his:ComponentA>
甚至,這裡的字首還可以是個簡稱別名,比如yours=com.aaa.productA
,這可能只有複雜到一定程度才會出現,大家不要以為這太誇張,但總有一天Web
體系能構建超大型軟體,到那時候你就知道是不是可能了。
樣式的區域性作用域
這個前一段時間有的瀏覽器實現過,在元件內部,style
上加一個scoped
屬性,這是正確的方向。為什麼要這麼幹呢,所謂元件,引入成本越小越好,在無約定的情況下都能引入,不造成問題,那是最佳的結果。
如果你一個元件的樣式不是區域性的,很可能就跟主介面的衝突了,就算你不跟主介面的衝突,怎麼保證不跟主介面中包含的其他元件的樣式衝突?靠命名約定是不現實的,看長遠一些,等你的系統夠大,這就是大問題了。
跟主文件的通訊
一個自定義元件,應當能夠跟主文件進行通訊,這個過程包括兩個方向,分別可以有多種不同的方式。
從內向外
除了事件,真沒有什麼好辦法可以做這個方向的通訊,但事件也可以有兩種定義方式,一種是類似onclick
那種,主文件應當能夠在它上面直接新增對應的事件監聽函式,就像對原生元素那樣,每個事件都能單獨使用。另一種是像postMessage
那樣,只提供一個通道,具體怎麼處理,自己去定義訊息格式和處理方式。
這兩種實現方式都可行,後者比較偷懶,但也夠用了,前者也沒有明顯優勢。
從外向內
這個也可以有兩種方式,一種是元件對外暴露屬性或者方法,讓主文件呼叫,一種是外部也通過postMessage
往裡傳。前者用起來會比較方便,後者也能湊合用用。
所以,如果特別偷懶,這個元件就變得像一個iframe
那樣,跟外部基本都通過postMessage
互動。
JavaScript
寫到這裡我是很糾結的,因為終於來到爭議最大的地方了。按照很多人的思路,我這裡應該也寫隔離成區域性作用域的JavaScript
才對,但真不行,我們可以先假設元件內部的所有JavaScript
都跑在區域性作用域,它不能訪問主文件中的物件。
我這裡解釋一下之前那個坑,為什麼端到端元件是有缺陷的。
先解釋什麼叫端到端元件。比如說,我有這麼一個元件,它封裝了對後端某介面的呼叫,還有自身的一些展示處理,跟外界通過事件通訊。它整個是不需要依賴別人的,初始載入資料都是自己內部做,別人要用它也很簡單,直接拿來放在頁面裡就可以了。
照理說,這東西應當非常好才對,使用起來這麼方便,到底哪裡不對?我來舉個場景。
在頁面上同時存在這個元件的多個例項,每個元件都去載入了初始資料,假設它們是不帶引數的,每個元件載入的資料都一樣,這裡是不是就有浪費的請求了?有人可能覺得一點點浪費不算問題,那麼繼續。
假設這個元件就是一個很普通的下拉選單,用於選取人員的職業,初始可能有醫生,教師,警察等等,我把這個元件直接放在介面上,它一出現,就自己去載入了所需的列表資訊並且展示了。有另外一個配置介面,用於配置這些職業資訊,這時候我在裡面新增了一個護士,並且提交了。假設為了資料一致性,我們把這個變更推回到頁面,麻煩就出現了。
介面只有一個職業下拉選單的時候可能還好辦,有多個的時候,這個更新的策略就有問題了。
如果在元件的內部做這個推送的對接,就會出現要推送多份一致的資料給元件的不同例項的問題。如果把這個放在外面,那我們也有兩種方式:
訂閱釋出模式,元件訂閱某個資料來源,資料來源跟服務端對接,當資料變更的時候,發給每個訂閱者
觀察者模式,元件觀察某個資料來源,當資料變更的時候,去取回來
這兩種很類似,不管哪種,都面臨一個問題:
資料來源放在哪?
很明顯不能放在元件內部了,只能放在某個“全域性”的地方,但剛才我們假設的是,元件內部的JavaScript
程式碼不能訪問外界的物件,所以……
但要是讓它能訪問,元件的隔離機制等於白搭。最好的方式,也許是兩種都支援,預設是區域性作用域,另外專門有一個作用域放給JS
框架之類的東西用,但瀏覽器實現的難度可能就大了不少。
可能有人會說,你怎麼把問題搞這麼複雜,用這麼BT
的場景來給我們美好的未來出難題。我覺得問題總是要面對的,能在做出來之前就面對問題,結果應該會好一些。
我注意觀察了很多朋友對Web Components
的態度,大部分都是完全叫好,但其中有一部分,主要是搞前端MV*
的同學對它的態度很保守,主要原因應該是我說的這幾點。因為這個群體主要都在做單頁型的應用,這個裡面會遇到的問題是跟傳統前端不同的。
那麼,比如Angular
,或者React
,它們跟Web Components
的協作點在哪裡呢?我個人覺得是把引擎保留下來,上層部分逐步跟Web Components
融合,所以它們不是誰吃掉誰的問題,而是怎樣去融合。最終就是在前端有兩層,一層是資料和業務邏輯層,一層是偏UI
的,在那個層裡面,可以存在像Web Components
那樣的垂直切分,這樣會很適宜。
最後說說自己對Polymer
的意見,我的看法沒有@司徒正美 那麼粗暴,但我是認同他的觀點的,因為Polymer
的根本理念就是在做端到端元件,它會面臨很多的挑戰。雖然它是一個元件化框架,元件化最適宜於解決大規模協作問題,但是如果是以走向大型單頁應用這條路來看,它比Angular
和React
離目標的距離還遠很多。
尤雨溪的理解
1、Web Components
中 Custom Elements
規範裡命名必須帶一個短橫 -
,這基本上是起到強制名稱空間的意義。連結
2、Shadow DOM
內的樣式預設就是區域性作用域的。連結
3、通訊
從內向外:就是沿用現有的 DOM
事件 API
。
從外向內:Polymer
的做法是通過暴露公開的屬性 (attributes
),然後用 MutationObserver
來觀察屬性的變化,並且保持對應的 property
和 attribute
之間的同步。用法上是 <input type="xxx" value="xxx">
這樣的感覺。至於動態的資料,Web Components
規範本身其實不涉及資料的繫結和傳遞,具體實現還是由框架來執行。
4、資料來源問題,我覺得好像沒什麼特別難的,在一個閉包裡定義共享的部分和建構函式即可。舉個簡單的例子:
(function () {
var sharedStore = new Store() // 共享的資料推送/抓取服務,可以包含快取機制
var customElementProto = Object.create(HTMLElement.prototype)
customElementProto.createdCallback = function () {
this.data = sharedStore.fetch() // 呼叫共享的資料
}
document.registerElement('my-element', {
prototype: customElementProto
})
})()
5、需要明確一點,那就是 Web Components
作為一套規範並不試圖取代框架的職責,而只是提供一套和現有 DOM API
風格一致的封裝機制。每個 component
內部可以採用不同的實現,但是不管你內部用什麼框架,對外暴露的都是一樣的 API
。所以你可以在框架 A
寫的應用裡很輕鬆地使用內部由框架 B
實現的元件。這樣元件複用的一大難題:框架不相容的問題就被抹平了。個人認為,這才是 Web Components
的核心意義所在。對於框架作者來說,並不需要針對 Web Component
做出很大的調整,只需要提供一個註冊用自身實現的 Web Component
的便利函式就足夠了。
6、Polymer
和 Web Components
不是一回事,Web Components
是一套規範,Polymer
是一個框架。Polymer
本身也分三部分:
- 抹平瀏覽器差異,實現
Web Components
規範的polyfill
- 核心框架。其實就是基於
DOM
物件本身的MVVM
,理念是 "DOM
就是ViewModel
"。 - 在核心框架基礎上附加的元件系統。
在核心框架的層面上,Polymer
和 Angular
、React
雖然各自實現有很大的差異,但從功能的角度來看其實是差不多的。