1. 程式人生 > >前端元件化框架之路

前端元件化框架之路

1. 為什麼元件化這麼難做

Web應用的元件化是一個很複雜的話題。

在大型軟體中,元件化是一種共識,它一方面提高了開發效率,另一方面降低了維護成本。但是在Web前端這個領域,並沒有很通用的元件模式,因為缺少一個大家都能認同的實現方式,所以很多框架/庫都實現了自己的元件化方式。

前端圈最熱衷於造輪子了,沒有哪個別的領域能出現這麼混亂而欣欣向榮的景象。這一方面說明前端領域的創造力很旺盛,另一方面卻說明了基礎設施是不完善的。

我曾經有過這麼一個類比,說明某種程式設計技術及其生態發展的幾個階段:

  • 最初的時候人們忙著補全各種API,代表著他們擁有的東西還很匱乏,需要在語言跟基礎設施上繼續完善

  • 然後就開始各種模式,標誌他們做的東西逐漸變大變複雜,需要更好的組織了

  • 然後就是各類分層MVC,MVP,MVVM之類,視覺化開發,自動化測試,團隊協同系統等等,說明重視生產效率了,也就是所謂工程化

那麼,對比這三個階段,看看關注這三種東西的人數,覺得Web發展到哪一步了?

細節來說,大概是模組化和元件化標準即將大規模落地(好壞先不論),各類API也大致齊備了,終於看到起飛的希望了,各種框架幾年內會有非常強力的洗牌,如果不考慮老舊瀏覽器的拖累,這個洗牌過程將大大加速,然後才能釋放Web前端的產能。

但是我們必須注意到,現在這些即將普及的標準,很多都會給之前的工作帶來改變。用工業體系的發展史來對比,前端領域目前正處於蒸汽機發明之前,早期機械(比如《木蘭辭》裡面的機杼,主要是動力與材料比較原始)已經普及的這麼一個階段。

所以,從這個角度看,很多框架/庫是會消亡的(專門做模組化的AMD和CMD相關庫,專注於標準化DOM選擇器鋪墊的某些庫),一些則必須進行革新,還有一些受的影響會比較小(資料視覺化等相關方向),可以有機會沿著自己的方向繼續演進。

2. 標準的變革

對於這類東西來說,能獲得廣泛群眾基礎的關鍵在於:對將來的標準有怎樣的迎合程度。對前端程式設計方式可能造成重大影響的標準有這些:

  • module

  • Web Components

  • class

  • observe

  • promise

module的問題很好理解,JavaScript第一次有了語言上的模組機制,而Web Components則是約定了基於泛HTML體系構建元件庫的方式,class增強了程式設計體驗,observe提供了資料和展現分離的一種優秀方式,promise則是目前前端最流行的非同步程式設計方式。

這裡面只有兩個東西是繞不過去的,一是module,一是Web Components。前者是模組化基礎,後者是元件化的基礎。

module的標準化,主要影響的是一些AMD/CMD的載入和相關管理系統,從這個角度來看,正如seajs團隊的@afc163所說,不管是AMD還是CMD,都過時了。

模組化相對來說,遷移還比較容易,基本只是純邏輯的包裝,跟AMD或者CMD相比,包裝形式有所變化,但元件化就是個比較棘手的問題了。

Web Components提供了一種元件化的推薦方式,具體來說,就是:

  • 通過shadow DOM封裝元件的內部結構

  • 通過Custom Element對外提供元件的標籤

  • 通過Template Element定義元件的HTML模板

  • 通過HTML imports控制組件的依賴載入

這幾種東西,會對現有的各種前端框架/庫產生很巨大的影響:

  • 由於shadow DOM的出現,元件的內部實現隱藏性更好了,每個元件更加獨立,但是這使得CSS變得很破碎,LESS和SASS這樣的樣式框架面臨重大挑戰。

  • 因為元件的隔離,每個元件內部的DOM複雜度降低了,所以選擇器大多數情況下可以限制在元件內部了,常規選擇器的複雜度降低,這會導致人們對jQuery的依賴下降。

  • 又因為元件的隔離性加強,致力於建立前端元件化開發方式的各種框架/庫(除Polymer外),在自己的元件實現方式與標準Web Components的結合,元件之間資料模型的同步等問題上,都遇到了不同尋常的挑戰。

  • HTML imports和新的元件封裝方式的使用,會導致之前常用的以JavaScript為主體的各類元件定義方式處境尷尬,它們的依賴、載入,都面臨了新的挑戰,而由於全域性作用域的弱化,請求的合併變得困難得多。

3. 當下最時髦的前端元件化框架/庫

在2015年初這個時間點看,前端領域有三個框架/庫引領時尚,那就是Angular,Polymer,React(排名按照首字母),在知乎的這篇《2014年末有哪些比較火的Web開發技術?》裡,我大致回答過一些點,其他幾位朋友的答案也很值得看。關於這三者的細節分析,侯振宇的這篇講得很好:2015前端框架何去何從

我們可以看到,Polymer這個東西在這方面是有先天優勢的,因為它的核心理念就是基於Web Components的,也就是說,它基本沒有考慮如何解決當前的問題,直接以未來為發展方向了。

React的程式設計模式其實不必特別考慮Web標準,它的遷移成本並不算高,甚至由於其實現機制,遮蔽了UI層實現方式,所以大家能看到在native上的使用,canvas上的使用,這都是與基於DOM的程式設計方式大為不同的,所以對它來說,處理Web Components的相容問題要在封裝標籤的時候解決,反正之前也是要封裝。

Angular 1.x的版本,可以說是跟同時代的多數框架/庫一樣,對未來標準的相容基本沒有考慮,但是重新規劃之後的2.0版本對此有了很多權衡,變成了激進變更,突然就變成一個未來的東西了。

這三個東西各有千秋,在可以預見的幾年內將會鼎足三分,也許還會有新的框架出現,能不能比這幾個流行就難說了。

此外,原Angular 2.0的成員Rob Eisenberg建立了自己的新一代框架aurelia,該框架將成為Angular 2.0強有力的競爭者。

4. 前端元件的複用性

看過了已有的一些東西之後,我們可以大致來討論一下前端元件化的一些理念。假設我們有了某種底層的元件機制,先不管它是瀏覽器原生的,或者是某種框架/庫實現的約定,現在打算用它來做一個大型的Web應用,應該怎麼做呢?

所謂元件化,核心意義莫過於提取真正有複用價值的東西。那怎樣的東西有複用價值呢?

  • 控制元件

  • 基礎邏輯功能

  • 公共樣式

  • 穩定的業務邏輯

對於控制元件的可複用性,基本上是沒有爭議的,因為這是實實在在的通用功能,並且比較獨立。

基礎邏輯功能主要指的是一些與介面無關的東西,比如underscore這樣的輔助庫,或者一些校驗等等純邏輯功能。

公共樣式的複用性也是比較容易認可的,因此也會有bootstrap,foundation,semantic這些東西的流行,不過它們也不是純粹的樣式庫了,也帶有一些小的邏輯封裝。

最後一塊,也就是業務邏輯。這一塊的複用是存在很多爭議的,一方面是,很多人不認同業務邏輯也需要元件化,另一方面,這塊東西究竟怎樣去元件化,也很需要思考。

除了上面列出的這些之外,還有大量的業務介面,這塊東西很顯然複用價值很低,基本不存在複用性,但仍然有很多方案中把它們“元件化”了,使得它們成為了“不具有複用性的元件”。為什麼會出現這種情況呢?

元件化的本質目的並不一定是要為了可複用,而是提升可維護性。這一點正如面嚮物件語言,Java要比C++純粹,因為它不允許例外情況的出現,連main函式都必須寫到某個類裡,所以Java是純面嚮物件語言,而C++不是。

在我們這種情況下,也可以把元件化分為:全元件化,區域性元件化。怎麼理解這兩個東西的區別呢,有人問過js框架和庫的區別是什麼,一般來說,有某種較強約定的東西,稱為框架,而約定比較鬆散的,稱為庫。框架很多都是有全元件化理念的,比如說,很多年前就出現的ExtJS,它是全元件化框架,而jQuery和它的外掛體系,則是區域性元件化。所以用ExtJS寫東西,不管寫什麼都是差不多一樣的寫法,而用jQuery的時候,大部分地方是原始HTML,哪裡需要有些不一樣的東西,就只在那個地方呼叫外掛做一下特殊化。

對於一個有一定規模的Web應用來說,把所有東西都“元件化”,在管理上會有較大的便利性。我舉個例子,同樣是編寫程式碼,短程式碼明顯比長程式碼的可讀性更高,所以很多語言裡會建議“一個方法一般不要超過多少行,一個類最好不要超過多少行”之類。在Web前端這個體系裡,JavaScript這塊是做得相對較好的,現在入門水平的人,也已經很少會有把一堆js都寫在一起的了。CSS這塊,最近在SASS,LESS等框架的引領下,也逐步往模組化方面發展,否則直接編寫bootstrap那種css,會非常痛苦。

這個時候我們再看HTML的部分,如果不考慮模板等技術的使用,某些介面光佈局程式碼寫起來就非常多了,像一些表單,都需要一層套一層,很多簡單的表單元素都需要套個三層左右,更不必說一些有複雜佈局的東西了。尤其是整個系統單頁化之後,介面的header,footer,各種nav或者aside,很可能都有一定複雜性。如果這些東西的程式碼不作切分,那麼主介面的HTML一定比較難看。

我們先不管用什麼方式切分了,比如用某種模板,用類似Angular中的include,或者Polymer,React中的標籤,或者直接使用原生Web Components,總之是把一塊一塊都拆開了,然後包含進來。從這個角度看,這些拆出去的東西都像元件,但如果從複用性的角度看,很可能多數東西,每一塊都只有一個地方用,壓根沒有複用度。這個拆出去,純粹是為了使得整個工程易於管理,易於維護。

這時候我們再來關注不同框架/庫對UI層元件化的處理方式,發現有兩個型別,模板和函式。

模板是一種很常見的東西,它用HTML字串的方式表達介面的原始結構,然後通過代入資料的方式生成真正的介面,有的是生成目標HTML,有的還生成各種事件的自動繫結。前者是靜態模板,後者是動態模板。

另外有一些框架/庫偏愛用函式邏輯來生成介面,早期的ExtJS,現在的React(它內部還是可能使用模板,而且對外提供的是元件建立介面的進一步封裝——jsx)等,這種實現技術的優勢是不同平臺上程式設計體驗一致,甚至可以給每種平臺封裝相同的元件,呼叫方輕鬆寫一份程式碼,在Web和不同Native平臺上可用。但這種方式也有比較麻煩的地方,那就是介面調整比較繁瑣。

本文前面部分引用侯振宇的那篇文章裡,他提出這些問題:

如何能把元件變得更易重用? 具體一點:

  • 我在用某個元件時需要重新調整一下元件裡面元素的順序怎麼辦?

  • 我想要去掉元件裡面某一個元素怎麼辦? 如何把元件變得更易擴充套件? 具體一點:

  • 業務方不斷要求給元件加功能怎麼辦?

為此,還提出了“模板複寫”方案,在這一點上我有不同意見。

我們來看看如何把一個業務介面切割成元件。

有這麼一個簡單場景:一個僱員列表介面包括兩個部分,僱員表格和用於填寫僱員資訊的表單。在這個場景下,存在哪些元件?

對於這個問題,主要存在兩種傾向,一種是僅僅把“控制元件”和比較有通用性的東西封裝成元件,另外一種是整個應用都元件化。

對前一種方式來說,這裡面只存在資料表格這麼一個元件。

對後一種方式來說,這裡面有可能存在:資料表格,僱員表單,甚至還包括僱員列表介面這麼一個更大的元件。

這兩種方式,就是我們之前所說的“區域性元件化”,“全元件化”。

我們前面提到,全元件化在管理上是存在優勢的,它可以把不同層面的東西都搞成類似結構,比如剛才的這個業務場景,很可能最後寫起來是這個樣子:

QQ截圖20151119103742.jpg

對於UI層,最好的元件化方式是標籤化,比如上面程式碼中就是三個標籤表達了整個介面。但我個人堅決反對濫用標籤,並不是把各種東西都儘量封裝就一定好。

全標籤化的問題主要有這些:

  • 第一,語義化代價太大。只要用了標籤,就一定需要給它合適的語義,也就是命名。但實際用的時候,很可能只是為了把一堆html簡化一下而已,到底簡化出來的那東西應當叫什麼名字,光是起名也費不知多少腦細胞。比如你說僱員管理的表單,這個表單有heading嗎,有footer嗎,能摺疊嗎,等等,很難起一個讓別人一看就知道的名字,要麼就是特別長。這還算簡單的,因為我們是全元件化,所以很可能會有組合了多種東西的一個較複雜的介面,你想來想去也沒法給它起個名字,於是寫了個:

QQ截圖20151119103838.jpg

這尼瑪……可能我誇張了點,但很多時候專案規模夠大,你不起這麼複雜的名字,最後很可能沒法跟功能類似的一個元件區分開,因為這些該死的元件都存在於同一個名稱空間中。如果僅僅是當作一個介面片段來include,就不存在這種心理負擔了。

比如Angular裡面的這種:

QQ截圖20151119103856.jpg

就不給它什麼名字,直接include進來,用檔案路徑來區分。這個片段的作用可以用其目錄結構描述,也就是通過物理名而非邏輯名來標識,目錄層次充當了一個很好的名稱空間。

現在的一些主流MVVM框架,比如knockout,angular,avalon,vue等等,都有一種“介面模板”,但這種模板並不僅僅是模板,而是可以視為一種配置檔案。某一塊介面模板描述了自身與資料模型的關係,當它被解析之後,按照其中的各種設定,與資料建立關聯,並且反過來再更新自身所對應的檢視。

不含業務邏輯的UI(或者是業務邏輯已分離的UI)基本不適合作為元件來看待,因為即使在邏輯不變的情況下,介面改版的可能性也太多了。比如即使是換了新的CSS實現方式,從float佈局改成flex佈局,都有可能把DOM結構少套幾層div,因此,在使用模板的方案中,只能把介面層視為配置檔案,不能看成元件,如果這麼做,就會輕鬆很多。

部隊行軍的時候講究“逢山開路,遇水搭橋”,這句話的重點在於只有到某些地形才開路搭橋,使用MVVM這類模式解決的業務場景,多數時候是一馬平川,橫著走都可以,不必硬要造路。所以從整個方案看的話,UI層實現應該是模板與控制元件並存,大部分地方是模板,少數地方是需要單獨花時間搞的路和橋。

  • 第二,配置過於複雜。有很多東西其實不太適合封裝,不但封裝的代價大,使用的代價也會很大。有時候會發現,呼叫程式碼的絕大部分都是在寫各種配置。

就像剛才的僱員表單,既然你不從標籤的命名上去區分,那一定會在元件上加配置。比如你原來想這樣:

QQ截圖20151119103913.jpg

然後在元件內部,判斷有沒有設定heading,如果沒有就不顯示,如果有,就顯示。過了兩天,產品問能不能把heading裡面的某幾個字加粗或者換色,然後碼農開始允許這個heading屬性傳入html。沒多久之後,你會驚奇地發現有人用你的元件,沒跟你說,就在heading裡面傳入了摺疊按鈕的html,並且用選擇器給摺疊按鈕加了事件,點一下之後還能摺疊這個表單了……

然後你一想,這個不行,我得給他再加個配置,讓他能很簡單地控制摺疊按鈕的顯示,但是現在這麼寫太不直觀,於是採用物件結構的配置:

QQ截圖20151119103929.jpg

然後又有一天,發現有很多面板都可以摺疊,然後特意建立了一個可摺疊面板元件,又建立了一種繼承機制,其他普通業務面板從它繼承,從此一發不可收拾。

我舉這例子的意思是為了說明什麼呢,我想說,在規模較大的專案中,企圖用全標籤化加配置的方式來描述所有的普通業務介面,是一定事倍功半的,並且這個規模越大就越坑,這也正是ExtJS這類對UI層封裝過度的體系存在的最大問題。

這個問題討論完了,我們來看看另外一個問題:如果UI元件有業務邏輯,應該如何處理。

比如說,性別選擇的下拉框,它是一個非常通用化的功能,照理說是很適合被當做元件來提供的。但是究竟如何封裝它,我們就有些犯難了。這個元件裡除了介面,還有資料,這些資料應當內建在元件裡嗎?理論上從元件的封裝性來說,是都應當在裡面的,於是就這麼造了一個元件:

QQ截圖20151119103948.jpg

這個元件非常美好,只需直接放在任意的介面中,就能顯示帶有性別資料的下拉框了。性別的資料很自然地是放在元件的實現內部,一個寫死的陣列中。這個太簡單了,我們改一下,改成商品銷售的國家下拉框。

表面上看,這個沒什麼區別,但我們有個要求,本公司商品銷售的國家的資訊是統一配置的,也就是說,這個資料來源於服務端。這時候,你是不是想把一個http請求封裝到這元件裡?

這樣做也不是不可以,但存在至少兩個問題:

如果這類元件在同一個介面中出現多次,就可能存在請求的浪費,因為有一個元件例項就會產生一個請求。

如果國家資訊的配置介面與這個元件同時存在,當我們在配置介面中新增一個國家了,下拉框元件中的資料並不會實時重新整理。

第一個問題只是資源的浪費,第二個就是資料的不一致了。曾經在很多系統中,大家都是手動重新整理當前頁面來解決這問題的,但到了這個時代,人們都是追求體驗的,在一個全元件化的解決方案中,不應再出現此類問題。

如何解決這樣的問題呢?那就是引入一層Store的概念,每個元件不直接去到服務端請求資料,而是到對應的前端資料快取中去獲取資料,讓這個快取自己去跟服務端保持同步。

所以,在實際做方案的過程中,不管是基於Angular,React,Polymer,最後肯定都做出一層Store了,不然會有很多問題。

5. 為什麼MVVM是一種很好的選擇

我們回顧一下剛才那個下拉框的元件,發現存在幾個問題:

  • 介面不好調整。剛才的那個例子相對簡單,如果我們是一個省市縣三級聯動的元件,就比較麻煩了。比如說,我們想要把水平佈局改成垂直的,又或者,想要把中間的label的字改改,都會非常麻煩。按照傳統的做元件的方式,就要加若干配置項,然後元件裡面去分別判斷,修改DOM結構。

  • 如果資料的來源不是靜態json,而是某個動態的服務介面,那用起來就很麻煩。

  • 我們更多地需要業務邏輯的複用和純“控制元件”的複用,至於那些繫結業務的介面元件,複用性其實很弱。

所以,從這些角度,會盡量期望在HTML介面層與JavaScript業務邏輯之間,存在一種分離。

這時候,再看看絕大多數介面元件存在什麼問題:

有時候我們考慮一下DOM操作的型別,會發現其實是很容易列舉的:

  • 建立並插入節點

  • 移除節點

  • 節點的交換

  • 屬性的設定

多數介面元件封裝的絕大部分內容不過是這些東西的重複。這些東西,其實是可以通過某些配置描述出來的,比如說,某個陣列以什麼形式渲染成一個select或者無序列表之類,當陣列變動,這些東西也跟著變動,這些都應當被自動處理,如果某個方案在現在這個時代還手動操作這些,那真的是一種落伍。

所以我們可以看到,以Angular,Knockout,Vue,Avalon為代表的框架們在這方面做了很多事,儘管理念有所差異,但大方向都非常一致,也就是把大多數命令式的DOM操作過程簡化為一些配置。

有了這種方式之後,我們可以追求不同層級的複用:

  • 業務模型因為是純邏輯,所以非常容易複用

  • 檢視模型基本上也是純邏輯,介面層多數是純字串模板,同一個檢視模型搭配不同的介面模板,可以實現檢視模型的複用

  • 同一個介面模板與不同的檢視模型組合,也能直接組合出完全不同的東西

所以這麼一來,我們的複用粒度就非常靈活了。正因為這樣,我一直認為Angular這樣的框架戰略方向是很正確的,雖然有很多戰術失誤。我們在很多場景下,都是需要這樣的高效生產手段的。

6. 元件的長期積累

我們做元件化這件事,一定是一種長期打算,為了使得當前的很多東西可以作為一種積累,在將來還能繼續使用,或者僅僅作較小的修改就能使用,所以必須考慮對未來標準的相容。主要需要考慮的方面有這幾點:

  • 儘可能中立於語言和框架,使用瀏覽器的原生特性

  • 邏輯層的模組化(ECMAScript module)

  • 介面層的元素化(Web Components)

之前有很多人對Angular 2.0的激進變更很不認同,但它的變更很大程度上是對標準的全面迎合。這不僅僅是它的問題,其實是所有前端框架的問題。不面對這些問題,不管現在多麼好,將來都是死路一條。這個問題的根源是,這幾個已有的規範約束了模組化和元素化的推薦方式,並且,如果要對當前和未來兩邊做適配的話,基本就沒法幹了,導致以前的都不得不做一定的遷移。

模組化的遷移成本還比較小,無論是之前AMD還是CMD的,都可以根據一些規則轉換過來,但元件化的遷移成本太大了,幾乎每種框架都會提出自己的理念,然後有不同的元件化理念。

還是從三個典型的東西來說:Polymer,React,Angular。

Polymer中的元件化,其實就是標籤化。這裡的標籤,並不只是介面元素,甚至邏輯元件也可以這樣,比如這個程式碼:

QQ截圖20151119104124.jpg

注意到這裡的core-ajax標籤,很明顯這已經是純邏輯的了,在大多數前端框架或者庫中,呼叫ajax肯定不是這樣的,但在瀏覽器端這麼幹也不是它獨創,比如flash裡面的WebService,比如早期IE中基於htc實現的webservice.htc等等,都是這麼幹的。在Polymer中,這類東西稱為非可見元素(non-visual-element)。

React的元件化,跟Polymer略有不同,它的介面部分是標籤化,但如果有單純的邏輯,還是純JavaScript模組。

既然大家的實現方式都那麼不一致,那我們怎麼搞出盡量可複用的元件呢?問題到最後還是要繞到Web Components上。

在Web Components與前端元件化框架的關係上,我覺得是這麼個樣子:

各種前端元件化框架應當儘可能以Web Components為基石,它致力於組織這些Components與資料模型之間的關係,而不去關注某個具體Component的內部實現,比如說,一個列表元件,它究竟內部使用什麼實現,元件化框架其實是不必關心的,它只應當關注這個元件的資料存取介面。

然後,這些元件化框架再去根據自己的理念,進一步對這些標準Web Components進行封裝。換句話說,業務開發人員使用某個元件的時候,他是應當感知不到這個元件內部究竟使用了Web Components,還是直接使用傳統方式。(這一點有些理想化,可能並不是那麼容易做到,因為我們還要管理像import之類的事情)。

7. 我們需要關注什麼

目前來看,前端框架/庫仍然處於混戰期,可比中國歷史上的春秋戰國,百家齊放,作為跟隨者來說,這是很痛苦的,因為無所適從,很可能你作為一個企業的前端架構師或者技術經理,需要做一些選型工作,但選哪個能保證幾年後不被淘汰呢?基本沒有。

雖然我們不知道將來什麼框架會流行,但我們可以從一些細節方面去關注,某個具體的方面,將來會有什麼,也可以瞭解一下在某個具體領域存在