這篇文章將從 AngularJS ReactJS Polymer 這幾個流行的框架入手,分析前端框架在這幾年發展中的關鍵技術點,作為2015前端技術選型的參考。摘要:
- 初體驗
- 技術特點
- 組件化
- 應用架構
- 總結
1. 初體驗
拿TODO來作為引子好了.
Angular 的實現
React的實現(非flux架構)
Polymer的實現
三者共同對比
在Angular中有controller和component的概念是分離的,而react和polymer中只有component的概念。
實際上三者在最簡單的使用場景下差異并不大,Angular和polymer模板和代碼分離的方式更貼近于傳統的前端做法,而React寫法更像后端渲染。關于學習和使用成本的誰高誰低得問題沒有什么好爭論的,在MVVM已經流行了這么久的情況下,三者入門門檻都差不多,但要用好都需要深入其中的運行機制才行。
2. 技術特點
實際上所謂的MVVM框架的關鍵技術就一個:數據與視圖的綁定。在Angular/polymer/knockout/vue/avalon 中,這項技術的實現又可以拆分成兩個關鍵點:模板分析和數據監測。
模板分析的主要目的是對 {{title}} 這樣的標記進行收集。收集完成之后生成一個視圖更新函數,在函數內部保存著這個標記所在的Dom片段和相關的數據名稱,函數被調用時會去重新取數據名稱對應的數據(或者由外部將相應的數據作為參數傳入),然后更新dom片段。這樣就實現了視圖的更新。一般框架會在啟動時就將模板分析完,生成相應的視圖更新函數。當數據更新的時候,就調用這些更新函數來更新視圖,那么問題來了,如何檢測數據的改動?
knockout/angular/avalon代表了三種方案:
- 使用自定義的數據對象及其指定的get和set函數。例如你只能使用 user.set("name","john")來給user對象的name屬性賦值,因為這樣它才能在set函數中知道修改了什么屬性,并且只調用相應的視圖更新函數。這種方式不太爽的地方在于改變了原有的JS對象使用的方式。
- 使用 Object.defineProperty 的get和set函數來檢測對象屬性的改動,本質上和上種沒有什么區別。但是它有一個缺陷,就是無法檢測新增的或刪除的屬性。有的框架是通過Object.observe來補充這種方案的,不過Object.observe 目前也只有chrome支持。這種方法改良了上面的開發體驗,你可以像使用原生JS對象一樣來操作你的數據。但是在實現上較為復雜。
- dirty check。這是angular正在使用的機制,它并不能像前兩種一樣一旦數據發生變化立即觸發更新回調。而是必須在調用了angular提供的一些方法,或者觸發了頁面上使用了ng-click等的元素上的事件后才會觸發。這些觸發時機是angular內部就已經實現了的,所以你幾乎感覺不到。這種方法被稱為"dirty"的原因是,它保存了所有屬性上一次的值,檢測是通過遍歷對象的所有屬性,對比它和上一次值是否一樣來實現的。如果是深層對象的話,它會層層遍歷。這種檢測方式結合了上面兩種的優勢,但是對性能造成了負擔。
至此,兩個關鍵技術點都已講清楚,用一張圖來回顧一下
而在React中則相對簡單,React用的是類似于重繪的機制,當觸發了 setState 之后,就完全重新渲染(并非立即觸發,中間有類似于緩存的性能提升機制)。這看起來比起前面的方案簡單粗暴,但是卻因為virtual dom的實現化腐朽為神奇了。virtual dom指的是React內部用來模擬真實dom的一種數據對象。當重新渲染時,實際上是先生成這樣virtual dom,然后將其和上一次的virtual dom進行對比,找出差異,最后由react在真實的dom上更新有差異的部分就夠了。因為virtual dom始終在內存中,真實的dom操作非常少,而前面的幾種框架在更新視圖時常常會有大量的dom操作,因此react在性能上大大領先前一種類型的框架。同時也因為virtual dom仍然是標準的 js對象,所以使得"服務端渲染"也成為可能。
值得注意的是,雖然React本身并不會像前面的框架一樣深入的去檢測數據的哪一部分發生了變化,但是可以通過官方提供的addon 和immutable.js來進一步提高這一塊的性能。
3. 組件化
在組件化的方向上 react 和其他幾種框架幾乎已經分道揚鑣了。從 angular2.0的設計和新出的 aurelia 等框架中可以看到大家都在嘗試往 webcomponent 靠近。polymer號稱下個版本代碼將大幅減少,那無非是因為瀏覽器將實現標準了。靠近 webcomponent 的好處在于任何一個框架都將不再封閉,以 custom element作為接口層,能實現生態圈的融合。
雖然 react 也有封裝成 custom element的方案,但是 react 并沒有很好的調用其他框架生成的 custom element 的方案。"像使用原生dom元素一樣使用custom element"的組件使用方式意味著尊重原生的dom使用方式,包括dom的事件等等。這和react"不操作真實dom"的基礎已經方向相悖了。
react和其他框架的分歧其實目前看來并無優劣之分,因為webcomponent目前除了chrome以外其他瀏覽器支持仍然不全面。另外考慮到特殊國情的話,大公司的產品仍然要面對IE8。不幸的是目前polymer的polyfill最低也只到IE9。而React能無痛支持IE8。再考慮到移動端的瀏覽器情況的話,也是使用react的技術阻力遠小于webcomponent。
總體來看,webcomponent肯定會是趨勢,并且將促進各個框架變得更加開放,更易互相融合。而react也仍將繼續依靠自己在實現上的優勢繼續走下去。也許未來在這中間又將誕生新東西。
暫時拋開react和webcomponent。我們繼續深入兩個目前討論得很少但是卻很重要的問題(下面討論的組件問題都以封裝成custom element為基礎):
- 如何能把組件變得更易重用? 具體一點:
- 我在用某個組件時需要重新調整一下組件里面元素的順序怎么辦?
- 我想要去掉組件里面某一個元素怎么辦?
- 如何把組件變得更易擴展? 具體一點:
- 業務方不斷要求給組件加功能怎么辦?
針對第一個問題,我所在的團隊目前提出一個叫做"模板復寫"的規則,這個規則又分為"完全重寫"和"部分重寫"兩種規則:
部分重寫
這種方案已在angular中實現。并且在組件重用率高的系統中已經驗證非常實用。但它也有缺陷,缺陷在于你必須知道當前組件的實現方式和原有模板才能復寫。
第二個問題,可以用一種稱為"共享作用域"的方式來解決。例如上面的例子中story沒有顯示like數量,現在要顯示出來。常規方案有兩種:
- 改組件,在組件中增加這個功能。
- 給組件增加api用于獲取統計數據,同時在統計數據發生變化時拋出事件通知外部。
第一種方案可能碰到的問題是當再次發生變化,例如統計數據不要顯示在組件里面了。就得繼續改成第二種方案。 第二種方案可能碰到的問題是可能不斷有新的需求提出來,最后不得不把每一個內部狀態都暴露出來,每一個操作過程都拋出事件。
"作用域共享"共享的方案是: 通過在一個特殊標記 "import-to" 將某一段外部html引入到某個組件中去一起參與"模板解析"和"數據綁定",當完成時再放回原來的位置。這樣這個外部html就能獲取到組件內部任何狀態和數據了。這種方案看起來有點像hack,但其實只是換了一種方式來理解組件:組件分成兩個部分,一是數據,二是視圖。視圖理論上應該只受到它的邏輯是否足夠內聚的約束,而不應該受到它的子元素是否放在一起的約束。但是目前我們剛好使用了dom作為視圖的基礎,所以視圖受到html結構的約束,這個約束是不合理的。我們來用圖對比一下使用"作用域共享"前后的場景:
當然,這種方案的缺陷仍然是你必須知道組件的具體實現。但這并不是一個不可克服的缺陷,我們看下aurelia的設計,它將template等等關鍵部分都設計成了可插拔的形式,這種結構意味著未來有可能實現一種通用的模板語法來實現上述兩個功能。這樣就不再和底層耦合。
4. 應用架構
應用架構的范圍太廣,我們這里只討論那些已經很好地組件化了的應用,或者是沒組件化但是有明確層級劃分的應用。我們以React 對應的 FLUX 為切入點。
我們再來結合facebook的官方FLUX代碼示例來看看每個部分:
facebook在介紹FLUX的時候的主要觀點是"MVC擴展性不夠,FLUX可擴展性高"。暫且不去討論FLUX與MVC的區別, 我們先來它是如何擴展的,從上面的代碼中可以看到,ACTION不只是一個界面上的點擊事件所產生的,ajax請求、甚至一個初始化過程都可以產生動作,"動作"只是一個抽象。動作將傳遞給dispatcher,有dispatcher在去觸發store注冊的回調。你可能會想,從這個dispatcher實際上什么也沒干,這和我直接定義一個方法,觸發事件就直接調用這個方法有什么區別?區別在于,當應用增加功能、進行擴展時,應用可能有多個部分要協同對同一個action進行響應,并且不同的協同部分可能在執行順序上有嚴格的先后之分。
舉個例子,如果我要對上面的TODO增加一個"統計區塊",如果是傳統的MVC寫法,你可能要新增一個statisticModel,然后在controller中的createTODO、deleteTODO中增加代碼來操作這個新的statisticModel。而FLUX不用修改已有的任何代碼,只需要寫新的store,并注冊一些回調到createAction、deleteAction中就夠了。所以可以看做是將MVC中的 "C主動操作M" 反轉成 "M來決定何時運行"(當然這種情況也就沒有C了), 但更好的是理解成是一種"事件系統"的變種。這就是它和MVC的區別。嚴格來說 FLUX 并不能算是facebook"發明"出來的,這樣的模型在很多事件驅動的后端框架中很常見,如zero、yii,只不過拿到前端來作為應用架構時比較新穎。
FLUX是目前高度推薦的應用架構方式,它并沒有強制使用的庫或者框架,所以并不局限于react,在angular、polymer中同樣能自由實現。特別是目前angular、polymer中的應用開發并沒有一種應用架構的最佳實踐。angular中的模塊化既沒有異步加載也沒有作用域隔離的作用,實際使用時很雞肋。但是angular中的依賴注入、filter、service的設計非常全面,如果再能加上FLUX的架構的話,威力不容小覷。對polymer來說情況更簡單,應為polymer目前只考慮到element這一層,所以上層的應用架構可以自由實現。
值得補充的是,FLUX中的store,dispatcher可以更好地加強一下。store可以使用一些自動支持REST的庫來簡化開發,dispatcher可以使用支持自定義順序等高級的事件代理實現。
5. 總結
2015將是前端框架相互借鑒相互融合的一年,隨著webcomponent的落地,大家都在像標準靠近。提前儲備這方面的技術肯定沒有問題。再深入到框架的技術細節中,我們看到在"渲染機制"、"數據綁定"、"組件化"、"模塊化"這些關鍵技術點中各個框架中都有非常精彩的實現,值得深入學習。React異軍突起,也推薦持續關注,特別是在"應用架構"上,FLUX確實在整個業界起到了啟發的作用,相信會越來越流行,并且有越來越多實現方式。
文章列表