分享:2015前端組件化框架之路

jopen 9年前發布 | 35K 次閱讀 組件化

分享:2015前端組件化框架之路

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平臺上可用。但這種方式也有比較麻煩的地方,那就是界面調整比較繁瑣。

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

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

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

  • 我想要去掉組件里面某一個元素怎么辦? 如何把組件變得更易擴展? 具體一點:

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

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

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

有這么一個簡單場景:一個雇員列表界面包括兩個部分,雇員表格和用于填寫雇員信息的表單。在這個場景下,存在哪些組件?

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

對前一種方式來說,這里面只存在數據表格這么一個組件。

對后一種方式來說,這里面有可能存在:數據表格,雇員表單,甚至還包括雇員列表界面這么一個更大的組件。

這兩種方式,就是我們之前所說的“局部組件化”,“全組件化”。

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

分享:2015前端組件化框架之路

對于UI層,最好的組件化方式是標簽化,比如上面代碼中就是三個標簽表達了整個界面。但我個人堅決反對濫用標簽,并不是把各種東西都盡量封裝就一定好。

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

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

分享:2015前端組件化框架之路

這尼瑪……可能我夸張了點,但很多時候項目規模夠大,你不起這么復雜的名字,最后很可能沒法跟功能類似的一個組件區分開,因為這些該死的組件都存在于同一個命名空間中。如果僅僅是當作一個界面片段來include,就不存在這種心理負擔了。

比如Angular里面的這種:

分享:2015前端組件化框架之路

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

現在的一些主流MVVM框架,比如knockout,angular,avalon,vue等等,都有一種“界面模板”,但這種模板并不僅僅是模 板,而是可以視為一種配置文件。某一塊界面模板描述了自身與數據模型的關系,當它被解析之后,按照其中的各種設置,與數據建立關聯,并且反過來再更新自身 所對應的視圖。

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

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

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

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

分享:2015前端組件化框架之路

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

然后你一想,這個不行,我得給他再加個配置,讓他能很簡單地控制折疊按鈕的顯示,但是現在這么寫太不直觀,于是采用對象結構的配置:

分享:2015前端組件化框架之路

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

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

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

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

分享:2015前端組件化框架之路

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

表面上看,這個沒什么區別,但我們有個要求,本公司商品銷售的國家的信息是統一配置的,也就是說,這個數據來源于服務端。這時候,你是不是想把一個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中的組件化,其實就是標簽化。這里的標簽,并不只是界面元素,甚至邏輯組件也可以這樣,比如這個代碼:

分享:2015前端組件化框架之路

注意到這里的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. 我們需要關注什么

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

雖然我們不知道將來什么框架會流行,但我們可以從一些細節方面去關注,某個具體的方面,將來會有什么,也可以了解一下在某個具體領域存在什么樣的方案。一個完整的框架方案,無非是以下多個方面的綜合。

7.1 模塊化

這塊還是不講了,支付寶seajs還有百度ecomfe這兩個團隊的人應該都能比我講得好得多。

7.2 Web Components

本文前面討論過一些,也不深入了。

7.3 變更檢測

我們知道,現代框架的一個特點是自動化,也就是把原有的一些手動操作提取。在前端編程中,最常見的代碼是在干什么呢?讀寫數據和操作DOM。不少 現代的框架/庫都對這方面作了處理,比如說通過某種配置的方式,由框架自動添加一些關聯,當數據變更的時候,把DOM進行相應修改,又比如,當DOM發生 變動的時候,也更新對應的數據。

這個關聯過程可能會用到幾種技術。首先我們看怎么知道數據在變化,這里面有三種途徑:

一、存取器的封裝。這個的意思也就是對數據進行一層包裝,比如:

var data = {
  name: "aaa",
    getName: function() {
    return this.name;
  },
  setName: function(value) {
    this.name = value;
  }
}

這樣,不允許用戶直接調用data.name,而是調用對應的兩個函數。Backbone就是通過這樣的機制實現數據變動觀測的,這種方式適用于幾乎所有瀏覽器,缺點就是比較麻煩,要對每個數據進行包裝。

這個機制在稍微新一點的瀏覽器中,也有另外一種實現方式,那就是defineProperty相關的一些方法,使用更優雅的存取器,這樣外界可以不用調用函數,而是直接用data.name這樣進行屬性的讀寫。

國產框架avalon使用了這個機制,低版本IE中沒有defineProperty,但在低版本IE中不止有JavaScript,還存在VBScript,那里面有存取器,所以他巧妙地使用了VBS做了這么一個兼容封裝。

基于存取器的機制還有個麻煩,就是每次動態添加屬性,都必須再添加對應的存取器,否則這個屬性的變更就無法獲取。

二、臟檢測。

以Angular 1.x為代表的框架使用了臟檢測來獲知數據變更,這個機制的大致原理是:

保存數據的新舊值,每當有一些DOM或者網絡、定時器之類的事件產生,用這個事件之后的數據去跟之前保存的數據進行比對,如果相同,就不觸發界面刷新,否則就刷新。

這個方式的理念是,控制所有可能導致數據變更的來源(也就是各種事件),在他們可能對數據進行操作之后,判斷新舊數據是否有變化,忽略所有中間變 更,也就是說,如果你在同一個事件中,把某個數據任意修改了很多次,但最后改回來了,框架會認為你什么都沒干,也就不會通知界面去刷新了。

不可否認的是,臟檢測的效率是比較低的,主要是不能精確獲知數據變更的影響,所以當數據量更大的情況下,浪費更嚴重,需要手動作一些優化。比如說 一個很大的數組,生成了一個界面上的列表,當某個項選中的時候,改變顏色。在這種機制下,每次改變這個項的數據狀態,就需要把所有的項都跟原來比較一遍, 然后,還要再全部比較一次發現沒有關聯引起的變化了,才能對應刷新界面。

三、觀察機制。

在ES7里面,引入了Object的observe方法,可以用于監控對象或數組的變動。

這是目前為止最合理的觀測方案。這個機制很精確高效,比如說,連長跟士兵說,你去觀察對面那個碉堡里面的動靜。這個含義很復雜,包括什么呢?

  • 是不是加人了

  • 是不是有人離開了

  • 誰跟誰換崗了

  • 上面的旗子從太陽旗換成青天白日了

所謂觀察機制,也就是觀測對象屬性的變更,數組元素的新增,移除,位置變更等等。我們先思考一下界面和數據的綁定,這本來就應當是一個外部的觀 察,你是數據,我是界面,你點頭我微笑,你伸手我打人。這種綁定本來就應當是個松散關系,不應當因為要綁定,需要破壞原有的一些東西,所以很明顯更合理。

除了數據的變動可以被觀察,DOM也是可以的。但是目前絕大多數雙向同步框架都是通過事件的方式把DOM變更同步到數據上。比如說,某個文本框綁定了一個對象的屬性,那很可能,框架內部是監控了這個文本框的鍵盤輸入、粘貼等相關事件,然后取值去往對象里寫。

這么做可以解決大部分問題,但是如果你直接myInput.value="111",這個變更就沒法獲取了。這個不算大問題,因為在一個雙向綁定 框架中,一個既被監控,又手工賦值的東西,本身也比較怪,不過也有一些框架會嘗試從HTMLInputELement的原型上去覆蓋value賦值,嘗試 把這種東西也納入框架管轄范圍。

另外一個問題,那就是我們只考慮了特定元素的特定屬性,可以通過事件獲取變更,如何獲得更廣泛意義上的DOM變更?比如說,一般屬性的變更,或者甚至子節點的增刪?

DOM4引入了MutationObserver,用于實現這種變更的觀測。在DOM和數據之間,是否需要這么復雜的觀測與同步機制,目前尚無定論,但在整個前端開發逐步自動化的大趨勢下,這也是一種值得嘗試的東西。

復雜的關聯監控容易導致預期之外的結果:

  • 慕容復要復國,每天讀書練武,各種謀劃

  • 王語嫣觀察到了這種現象,認為表哥不愛自己了

  • 段譽看到神仙姐姐悶悶不樂,每天也茶飯不思

  • 鎮南王妃心疼愛子,到處調查這件事的原委,意外發現段正淳還跟舊愛有聯系

……

總之這么下來,最后影響到哪里了都不知道,誰讓丘處機路過牛家村呢?

所以,變更的關聯監控是很復雜的一個體系,尤其是其中產生了閉環的時候。搭建整個這么一套東西,需要極其精密的設計,否則熟悉整套機制的人只要用 特定場景輕輕一推就倒了。靈智上人雖然武功過人,接連碰到歐陽鋒,周伯通,黃藥師,全部都是上來就直接被抓了后頸要害,大致就是這意思。

polymer實現了一個 observe-js ,用于觀測數組、對象和路徑的變更,有興趣的可以關注。

在有些框架,比如aurelia中,是混合使用了存取器和觀察模式,把存取器作為觀察模式的降級方案,在瀏覽器不支持observe的情況下使 用。值得一提的是,在臟檢測方式中,變更是合并后批量提交的,這一點常常被另外兩種方案的使用者忽視。其實,即使用另外兩種方式,也還是需要一個合并與批 量提交過程。

怎么理解這個事情呢?數據的綁定,最終都是要體現到界面上的,對于界面來說,其實只關注你每一次操作所帶來的數據變更的始終,并不需要關心中間過程。比如說,你寫了這么一個循環,放在某個按鈕的點擊中:

for (var i=0; i<10000; i++) { obj.a += 1;
}

界面有一個東西綁定到這個a,對框架來說,絕對不應當把中間過程直接應用到界面上,以剛才這個例子來說,合理的情況只應當存在一次對界面DOM的 賦值,這個值就是對obj.a進行了10000次賦值之后的值。盡管用存取器或者觀察模式,發現了對obj上a屬性的這10000次賦值過程,這些賦值還 是都必須被舍棄,否則就是很可怕的浪費。

React使用虛擬DOM來減少中間的DOM操作浪費,本質跟這個是一樣的,界面只應當響應邏輯變更的結束狀態,不應當響應中間狀態。這樣,如果 有一個ul,其中的li綁定到一個1000元素的數組,當首次把這個數組綁定到這個ul上的時候,框架內部也是可以優化成一次DOM寫入的,類似之前常用 的那種DocumentFragment,或者是innerHTML一次寫入整個字符串。在這個方面,所有優化良好的框架,內部實現機制都應當類似,在這 種方案下,是否使用虛擬DOM,對性能的影響都是很小的。

7.4 Immutable Data

Immutable Data是函數式編程中的一個概念,在前端組件化框架中能起到一些很獨特的作用。

它的大致理念是,任何一種賦值,都應當被轉化成復制,不存在指向同一個地方的引用。比如說:

var a = 1; var b = a;
b = 2;
console.log(a==b);

這個我們都知道,b跟a的內存地址是不一致的,簡單類型的賦值會進行復制,所以a跟b不相等。但是:

var a = {
    counter : 1 }; var b = a;
b.counter++;
console.log(a.counter==b.counter);

這時候因為a和b指向相同的內存地址,所以只要修改了b的counter,a里面的counter也會跟著變。

Immutable Data的理念是,我能不能在這種賦值情況下,直接把原來的a完全復制一份給b,然后以后大家各自變各自的,互相不影響。光憑這么一句話,看不出它的用處,看例子:

對于全組件化的體系,不可避免會出現很多嵌套的組件。嵌套組件是一個很棘手的問題,在很多時候,是不太好處理的。嵌套組件所存在的問題主要在于生 命周期的管理和數據的共享,很多已有方案的上下級組件之間都是存在數據共享的,但如果內外層存在共享數據,那么就會破壞組件的獨立性,比如下面的一個列表 控件:

分享:2015前端組件化框架之路

我們在賦值的時候,一般是在外層整體賦值一個類似數組的數據,而不是自己挨個在每個列表項上賦值,不然就很麻煩。但是如果內外層持有相同的引用,對組件的封裝性很不利。

比如在剛才這個例子里,假設數據源如下:

var arr = [
    {name: "Item1"},
    {name: "Item2"},
    {name: "Item3"}
];

通過類似這樣的方式賦值給界面組件,并且由它在內部給每個子組件分別進行數據項的賦值:

list.data = arr;

賦值之后會有怎樣的結果呢?

console.log(list.data == arr);
console.log(listitem.data == arr[]);
console.log(listitem1.data == arr[1]);
console.log(listitem2.data == arr[2]);

這種方案里面,后面那幾個log輸出的結果都會是true,意思就是內層組件與外層共享數據,一旦內層組件對數據進行改變,外層中的也就改變了,這明顯是違背組件的封裝性的。

所以,有一些方案會引入Immutable Data的概念。在這些方案里,內外層組件的數據是不共享的,它們的引用不同,每個組件實際上是持有了自己的數據,然后引入了自動的賦值機制。

這時候再看看剛才那個例子,就會發現兩層的職責很清晰:

  • 外層持有一個類似數組的東西arr,用于形成整個列表,但并不關注每條記錄的細節

  • 內層持有某條記錄,用于渲染列表項的界面

  • 在整個列表的形成過程中,list組件根據arr的數據長度,實例化若干個 listitem,并且把arr中的各條數據賦值給對應的listitem,而這個賦值,就是immutable data起作用的地方,其實是把這條數據復制了一份給里面,而不是把外層這條記錄的引用賦值進去。內層組件發現自己的數據改變之后,就去進行對應的渲染

  • 如果arr的條數變更了,外層監控這個數據,并且根據變更類型,添加或者刪除某個列表項

  • 如果從外界改變了arr中某一條記錄的內容,外層組件并不直接處理,而是給對應的內層進行了一次賦值

  • 如果列表項中的某個操作,改變了自身的值,它首先是把自己持有的數據進行改變,然后,再通過immutable data把數據往外同步一份,這樣,外層組件中的數據也就更新了。

所以我們再看這個過程,真是非常清晰明了,而且內外層各司其職,互不干涉。這是非常有利于我們打造一個全組件化的大型Web應用的。各級組件之間存在比較松散的聯系,而每個組件的內部則是封閉的,這正是我們所需要的結果。

說到這里,需要再提一個容易混淆的東西,比如下面這個例子:

分享:2015前端組件化框架之路

如果我們為了給inner-component做一些樣式定位之類的事情,很可能在內外層組件之間再加一些額外的布局元素,比如變成這樣:

分享:2015前端組件化框架之路

這里中間多了一級div,也可能是若干級元素。如果有用過Angular 1.x的,可能會知道,假如這里面硬造一級作用域,搞個ng-if之類,就可能存在多級作用域的賦值問題。在上面這個例子里,如果在最外層賦值,數據就會 是outer -> div -> inner這樣,那么,從框架設計的角度,這兩次賦值都應當是immutable的嗎?

不是,第一次賦值是非immutable,第二次才需要是,immutable賦值應當僅存在于組件邊界上,在組件內部不是特別有必要使用。剛才的例子里,依附于div的那層變量應當還是跟outer組件在同一層面,都屬于outer組件的人民內部矛盾。

這里是非死book實現的 immutable-js庫

7.6 Promise與異步

前端一般都習慣于用事件的方式處理異步,但很多時候純邏輯的“串行化”場景下,這種方式會讓邏輯很難閱讀。在新的ES規范里,也有yield為代 表的各種原生異步處理方案,但是這些方案仍然有很大的理解障礙,流行度有限,很大程度上會一直停留在基礎較好的開發人員手中。尤其是在瀏覽器端,它的受眾 應該會比node里面還要狹窄。

前端里面,處理連續異步消息的最能被廣泛接受的方案是promise,我這里并不討論它的原理,也不討論它在業務中的使用,而是要提一下它在組件化框架內部所能起到的作用。

現在已經沒有哪個前端組件化框架可以不考慮異步加載問題了,因為,在前端這個領域,加載就是一個繞不過去的坎,必須有了加載,才能有執行過程。每個組件化框架都不能阻止自己的使用者規模膨脹,因此也應當在框架層面提出解決方案。

我們可能會動態配置路由,也可能在動態加載的路由中又引入新的組件,如何控制這些東西的生命周期,值得仔細斟酌,如果在框架層面全異步化,對于編程體驗的一致性是有好處的。將各類接口都promise化,能夠在可維護性和可擴展性上提供較多便利。

我們之前可能熟知XMLHTTP這樣的通信接口,這個東西雖然被廣為使用,但是在優雅性等方面,存在一些問題,所以最近出來了替代方案,那就是fetch。

細節可以參見月影翻譯的這篇【翻譯】 這個API很“迷人”——(新的Fetch API)

在不支持的瀏覽器上,也有github實現的一個polyfill,雖然不全,但可以湊合用 window.fetch polyfill

大家可以看到,fetch的接口就是基于promise的,這應當是前端開發人員最容易接受的方案了。

7.7 Isomorphic JavaScript

這個東西的意思是前后端同構的JavaScript,也就是說,比如一塊界面,可以選擇在前端渲染,也可以選擇在后端渲染,值得關注,可以解決像seo之類的問題,但現在還不能處理很復雜的狀況,持續關注吧。

8. 小結

很感謝能看到這里,以上這些是我近一年的一些思考總結。從技術選型的角度看,做大型Web應用的人會很痛苦,因為這是一個青黃不接的年代,目前已 有的所有框架/庫都存在不同程度的缺陷。當你向未來看去,發現它們都是需要被拋棄,或者被改造的,人最痛苦的是在知道很多東西不好,卻又要從中選取一個來 用。@嚴清 跟@寸志 @題葉討論過這個問題,認為現在這個階段的技術選型難做,不如等一陣,我完全贊同他們的觀點。

選型是難,但是從學習的角度,可真的是挺好的時代,能學的東西太多了,我每天路上都在努力看有可能值得看的東西,可還是看不完,只能努力去跟上時代的步伐。

以下一段,與諸位共勉:

It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way--in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!