從HTML Components的衰落看Web Components的危機

記得要微笑發表於2014-10-01

搞前端時間比較長的同學都會知道一個東西,那就是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...

所以,大家都想辦法去提供自己的擴充元素的方式,現在我們是知道典型的有angularjspolymer,但很早的時候也不是沒有啊:

http://msdn.microsoft.com/en-...

看,這就是HTC的新增自定義元素的方式,每個元素可以定義自己對外提供的屬性、方法,還有事件,自己內部可以像寫一個新頁面一樣,專注於實現功能。而且你發現沒有,它考慮得很長遠,提供了名稱空間,防止你在一個頁面引入兩個不同組織提供的同名自定義元素。

這個東西就可以稱為元件了,它跟外界是完全隔離的,外界只要把它拿來就可以用,就像用原生元素一樣,用選擇器選擇,設定屬性,呼叫方法,新增事件處理等等,而且,注意到沒有,它的屬性是帶getset的,這是多麼夢寐以求的東西!

正是因為它這麼好用,所以在那個時代,我們用它幹了很多東西,封裝了各種基礎控制元件,比如樹,資料表格,日期選擇,等等,甚至當時也有人嫌棄瀏覽器原生selectradio不好看,用這麼個東西,裡面封裝了圖片來模擬功能,替換原生的來用。

當時也有人,比如我在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的根本理念就是在做端到端元件,它會面臨很多的挑戰。雖然它是一個元件化框架,元件化最適宜於解決大規模協作問題,但是如果是以走向大型單頁應用這條路來看,它比AngularReact離目標的距離還遠很多。

尤雨溪的理解

1、Web ComponentsCustom Elements 規範裡命名必須帶一個短橫 -,這基本上是起到強制名稱空間的意義。連結

2、Shadow DOM 內的樣式預設就是區域性作用域的。連結

3、通訊
從內向外:就是沿用現有的 DOM 事件 API

從外向內:Polymer 的做法是通過暴露公開的屬性 (attributes),然後用 MutationObserver 來觀察屬性的變化,並且保持對應的 propertyattribute 之間的同步。用法上是 <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、PolymerWeb Components 不是一回事,Web Components 是一套規範,Polymer 是一個框架。Polymer 本身也分三部分:

  • 抹平瀏覽器差異,實現 Web Components 規範的 polyfill
  • 核心框架。其實就是基於 DOM 物件本身的 MVVM,理念是 "DOM 就是 ViewModel"。
  • 在核心框架基礎上附加的元件系統。

在核心框架的層面上,PolymerAngularReact 雖然各自實現有很大的差異,但從功能的角度來看其實是差不多的。

相關文章