2015前端組件化框架之路(轉)

https://github.com/xufei/blog/issues/19css

1. 爲何組件化這麼難作

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

在大型軟件中,組件化是一種共識,它一方面提升了開發效率,另外一方面下降了維護成本。可是在Web前端這個領域,並無很通用的組件模式,由於缺乏一個你們都能認同的實現方式,因此不少框架/庫都實現了本身的組件化方式。 前端

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

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

  • 最初的時候人們忙着補全各類API,表明着他們擁有的東西還很匱乏,須要在語言跟基礎設施上繼續完善
  • 而後就開始各類模式,標誌他們作的東西逐漸變大變複雜,須要更好的組織了
  • 而後就是各種分層MVC,MVP,MVVM之類,可視化開發,自動化測試,團隊協同系統等等,說明重視生產效率了,也就是所謂工程化

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

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

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

因此,從這個角度看,不少框架/庫是會消亡的(專門作模塊化的AMD和CMD相關庫,專一於標準化DOM選擇器鋪墊的某些庫),一些則必須進行革新,還有一些受的影響會比較小(數據可視化等相關方向),能夠有機會沿着本身的方向繼續演進。 ajax

2. 標準的變革

對於這類東西來講,能得到普遍羣衆基礎的關鍵在於:對未來的標準有怎樣的迎合程度。對前端編程方式可能形成重大影響的標準有這些: spring

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

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

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

  • 我在用某個組件時須要從新調整一下組件裏面元素的順序怎麼辦?
  • 我想要去掉組件裏面某一個元素怎麼辦? 如何把組件變得更易擴展? 具體一點:
  • 業務方不斷要求給組件加功能怎麼辦?

爲此,還提出了「模板複寫」方案,在這一點上我有不一樣意見。

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

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

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

對前一種方式來講,這裏面只存在數據表格這麼一個組件。
對後一種方式來講,這裏面有可能存在:數據表格,僱員表單,甚至還包括僱員列表界面這麼一個更大的組件。

這兩種方式,就是咱們以前所說的「局部組件化」,「全組件化」。

咱們前面提到,全組件化在管理上是存在優點的,它能夠把不一樣層面的東西都搞成相似結構,好比剛纔的這個業務場景,極可能最後寫起來是這個樣子:

<Employee-Panel>
    <Employee-List></Employee-List>
    <Employee-Form></Employee-Form>
</Employee-Panel>

對於UI層,最好的組件化方式是標籤化,好比上面代碼中就是三個標籤表達了整個界面。但我我的堅定反對濫用標籤,並非把各類東西都儘可能封裝就必定好。

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

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

<Panel-With-Department-Panel-On-The-Left-And-Employee-Panel-On-The-Right>
</Panel-With-Department-Panel-On-The-Left-And-Employee-Panel-On-The-Right>

這尼瑪……可能我誇張了點,但不少時候項目規模夠大,你不起這麼複雜的名字,最後極可能無法跟功能相似的一個組件區分開,由於這些該死的組件都存在於同一個命名空間中。若是僅僅是看成一個界面片斷來include,就不存在這種心理負擔了。

好比Angular裏面的這種:

<div ng-include="'aaa/bbb/ccc.html'"></div>

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

如今的一些主流MVVM框架,好比knockout,angular,avalon,vue等等,都有一種「界面模板」,但這種模板並不只僅是模板,而是能夠視爲一種配置文件。某一塊界面模板描述了自身與數據模型的關係,當它被解析以後,按照其中的各類設置,與數據創建關聯,而且反過來再更新自身所對應的視圖。

不含業務邏輯的UI(或者是業務邏輯已分離的UI)基本不適合做爲組件來看待,由於即便在邏輯不變的狀況下,界面改版的可能性也太多了。好比即便是換了新的CSS實現方式,從float佈局改爲flex佈局,都有可能把DOM結構少套幾層div,所以,在使用模板的方案中,只能把界面層視爲配置文件,不能當作組件,若是這麼作,就會輕鬆不少。

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

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

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

<EmployeeForm heading="僱員表單"></EmployeeForm>

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

而後你一想,這個不行,我得給他再加個配置,讓他能很簡單地控制摺疊按鈕的顯示,可是如今這麼寫太不直觀,因而採用對象結構的配置:

<EmployeeForm>
    <Option collapsible="true">
        <Heading>
            <h4><strong>僱員</strong>表單</h4>
        </Heading>
    </Option>
</EmployeeForm>

而後又有一天,發現有不少面板均可以摺疊,而後特地建立了一個可摺疊面板組件,又建立了一種繼承機制,其餘普通業務面板從它繼承,今後一發不可收拾。

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

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

好比說,性別選擇的下拉框,它是一個很是通用化的功能,照理說是很適合被當作組件來提供的。可是究竟如何封裝它,咱們就有些犯難了。這個組件裏除了界面,還有數據,這些數據應當內置在組件裏嗎?理論上從組件的封裝性來講,是都應當在裏面的,因而就這麼造了一個組件:

<GenderSelect></GenderSelect>

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

表面上看,這個沒什麼區別,但咱們有個要求,本公司商品銷售的國家的信息是統一配置的,也就是說,這個數據來源於服務端。這時候,你是否是想把一個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中的組件化,其實就是標籤化。這裏的標籤,並不僅是界面元素,甚至邏輯組件也能夠這樣,好比這個代碼:

<my-panel>
    <core-ajax id="ajax" url="http://url" params="{{formdata}}" method="post"></core-ajax>
</my-panel>

注意到這裏的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發生變更的時候,也更新對應的數據。

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

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

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作了這麼一個兼容封裝。

基於存取器的機制還有個麻煩,就是每次動態添加屬性,都必須再添加對應的存取器,不然這個屬性的變動就沒法獲取。

2、髒檢測。

以Angular 1.x爲表明的框架使用了髒檢測來獲知數據變動,這個機制的大體原理是:

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

這個方式的理念是,控制全部可能致使數據變動的來源(也就是各類事件),在他們可能對數據進行操做以後,判斷新舊數據是否有變化,忽略全部中間變動,也就是說,若是你在同一個事件中,把某個數據任意修改了不少次,但最後改回來了,框架會認爲你什麼都沒幹,也就不會通知界面去刷新了。

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

3、觀察機制。

在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,而後之後你們各自變各自的,互相不影響。光憑這麼一句話,看不出它的用處,看例子:

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

<my-list list-data="{arr}">
    <my-listitem></my-listitem>
    <my-listitem></my-listitem>
    <my-listitem></my-listitem>
</my-list>

咱們在賦值的時候,通常是在外層總體賦值一個相似數組的數據,而不是本身挨個在每一個列表項上賦值,否則就很麻煩。可是若是內外層持有相同的引用,對組件的封裝性很不利。

好比在剛纔這個例子裏,假設數據源以下:

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

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

list.data = arr;

賦值以後會有怎樣的結果呢?

console.log(list.data == arr);
console.log(listitem0.data == arr[0]);
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應用的。各級組件之間存在比較鬆散的聯繫,而每一個組件的內部則是封閉的,這正是咱們所須要的結果。

說到這裏,須要再提一個容易混淆的東西,好比下面這個例子:

<outer-component>
    <inner-component></inner-component>
</outer-component>

若是咱們爲了給inner-component作一些樣式定位之類的事情,極可能在內外層組件之間再加一些額外的佈局元素,好比變成這樣:

<outer-component>
    <div>
        <inner-component></inner-component>
    </div>
</outer-component>

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

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

這裏是facebook實現的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.

相關文章
相關標籤/搜索