標籤:
原文地址:http://www.csdn.net/article/2015-08-04/2825370-react
2004年,對於前端社區來說,是裡程碑式的一年。Gmail橫空出世,它帶來基於前端渲染的原生應用層級的體驗,相對於之前的服務端渲染網頁可謂提升了一個時代,觸動了使用者的G點。自此,前端渲染的網站成為無數開發人員追逐的方向。
為了更好地開發前端渲染的“原生層級的”網站,包括Backbone和Angular在內的一系列前端架構應運而生,並迅速獲得了大規模的採用。但是很快地,新的效能和SEO問題也接踵而來。幾經嘗試後,Twitter甚至從前端渲染重回伺服器渲染,而Strikingly也面對過同樣棘手的問題。
2014年,React進入我們的視線。讓人耳目一新的是,對於其他開源架構遇到的種種問題,React都自信地給出瞭解答。幾乎沒有猶豫,我們開始使用React來重構Strikingly。若干年後,當我們回望,也許會發現,2014年也是前端社區裡程碑式的一年。
React簡介
React究竟是什嗎?Facebook把它簡單低調地定義成一個“用來構建UI的JavaScript庫”。這個定義也許會讓我們聯想到許多JavaScript範本語言(比如Handlebars和Swig),或者早期的控制項陳列庫(比如YUI和Dojo),但是React所基於的幾個核心概念使它與那些模板和控制項陳列庫迥然不同。事實上這幾個核心概念非常超前,已經給整個前端世界帶來了衝擊性的影響。它們包括:
- 組件和基於組件的設計流程;
- 單向資料流動;
- 虛擬DOM取代物理DOM作為操作對象;
- 用JSX文法取代HTML模板,在JavaScript裡聲明式地描述UI。
這幾條簡單的原則放在一起帶來了大量的好處:
- 前端和後端都能夠從React組件渲染頁面,完全解決了SEO長期困擾JavaScript單頁應用的問題;
- 我們可以簡單直接地寫前端測試而完全忘掉DOM依賴;
- 組件的封裝方式和單向資料流動能夠極大地簡化前端架構的理解難度。
我們來看一個例子:
[js] view plaincopy
- var HelloMessage = React.createClass({
- render: function() {
- return <div>Hello {this.props.name}</div>;
- }
- });
-
- React.render(<HelloMessage name="John" />, document.body);
這個React版的Hello World已經展現了React的一些核心特性。首先,HelloMessage是一個React組件;建立React應用的時候,我們總是以組件為出發點。每個組件的核心是一個render方法,在其中我們把這個組件的props和state拼裝到一個最終要渲染的模板中,然後返回這個模板(確切地說這裡是一個UI描述而不是傳統意義上的模板)。這段代碼裡看起來像HTML一樣的部分就是著名的JSX文法,它是在React中描述“模板”的最佳方式。
現在,以var開頭的第一段裡我們定義了一個叫HelloMessage的組件;下面的React.render這一行所做的,則是把這個組件渲染到document.body裡——也就是我們實際的頁面上。但在使用〈HelloMessage/〉的時候,我們做了另一件事:name="John"。看起來很像HTML中的元素屬性,但是既然JSX不是HTML,這個文法的作用是什麼呢?實際上,這就是我們向React組件傳入props的方式。回頭看第一段,我們可以看到在組件的內部有對this.props.name的引用。這個name就是我們剛剛指定的John!
看到這裡,如果你熟悉jQuery的話也許在想,這與$(document.body).html(‘Hello John‘) 有什麼根本區別呢?
這就是虛擬DOM出場的地方了。我們像寫HTML一樣寫JSX,但是JSX並不會直接變成HTML和DOM。在幕後,React維護著一個虛擬DOM,而實際上被瀏覽器直接操作的“物理”DOM只是這個虛擬DOM的投影。虛擬DOM不依賴於瀏覽器環境,它可以運行在任何JavaScript執行環境。這就讓下面的代碼成為可能:
[js] view plaincopy
- var html =React.renderToString(<HelloMessage name="John"/>);res.send(html);
如果第二行有點眼熟,你沒有猜錯——這段代碼發生在伺服器端!是的,同樣的 HelloMessage,我們不僅可以讓React在前端渲染到頁面,同樣可以在後端直接渲染成HTML字串,然後把它返回給前端。服務端預渲染就這麼自然地發生了。
React帶來的革命性創新是前端世界過去幾年最激動人心的變化。自從接觸React以來,我們深信React會徹底改變用戶端開發人員(包括前端、iOS和Android)的開發體驗。在下面的篇幅裡,我們想從四個大的方向——目標平台(Targets)、資料處理(Data)、工具(Tools)和新的挑戰——分享一下React生態系統和社區的進展和未來趨勢。
目標平台
對於虛擬DOM的討論,很多人會說速度快過於真正的DOM。這樣的討論可以讓人快速入門理解React,但是真正寫過React應用的人會明白速度並不是虛擬DOM的精髓。我們認為虛擬DOM的存在協助我們做到了兩件事。第一是申明式UI。通過虛擬DOM,UI不再是一個不斷被更變的DOM,你只要申明UI是怎麼產生的,React會自動幫你把UI的改變渲染到真正的DOM上。這種新的思維方式讓你可以不用手動操作真正的DOM。第二是多Target。我們一直在講Web,但React讓我們做到Web以外的Target。虛擬DOM更像是UI虛擬機器,自動幫你映射到真正的實現上,可以是瀏覽器DOM、iOS UI、Android UI。甚至有人做到了React映射到終端文本UI。
多Targets是React社區常常在討論的主要話題之一。多Targets的根本是提高開發人員體驗。開發人員體驗(DX,Developer Experience)是在React社區裡屢次被提起的概念。如何在保持一樣的使用者體驗下,提高開發人員體驗,是包括React在內的前端社區正在思考的問題。事實上任何一家有多用戶端的公司都面臨著這樣同一個問題:在各種用戶端語言裡重新造輪子。開發人員需要學習新的語言、寫和維護類似的功能。提升用戶端開發人員體驗就是減少學習成本和維護成本。這就是React提倡的“Learn once,write everywhere”。
最近也有一些鼓舞人心的訊息。Facebook內部Ads Manager iOS版本由7位前端工程師用React Native花了5個月完成。而Android版本,是同一班人,3個月內完成。代碼重用率達到了87%。
多Targets也可以是在單個平台更深度的結合。來自React核心團隊的Sebastian Markbåge在ReactEurope大會上給了一個讓人目瞪口呆的演講《DOM as a Second-class Citizen》。演講中他暢想React直接輸出到瀏覽器架構的底層(圖1瀏覽器的渲染架構,圖2為Sebastian Markbåge認為React可以做的事情)。
圖1 瀏覽器的渲染架構
圖2 Sebastian Markbåge認為React還可以做很多事情
姑且不談該不該這麼做,通過虛擬DOM開啟了這樣的機會就已經讓我們興奮不已了。也說明了Facebook在設計React時已經考慮到超越DOM。想法確實很超前。
【服務端預渲染(Pre-rendering)】
對於其他主流前端架構,頁面SEO和首次開啟速度的問題都很讓人頭疼。Twitter當年因為首次開啟速度過於慢甚至重回伺服器渲染方案。一直以來人們一直在尋找一種只需要編寫一次UI組件,前後端同時都能渲染的方案。如果能做到的話,我們就可以在首次開啟頁面時先用服務端渲染頁面HTML,當瀏覽器收到後已經可以顯示頁面。這樣SEO和首次開啟速度都能被解決。這種完美方案社區裡稱之為Isomorphic/Universal App。
React原生支援了Pre-rendering(服務端渲染)。由於有虛擬DOM,也就意味著我們只需要後端運行JavaScript引擎就能渲染整個DOM。目前主流後端語言都可以運行V8 JavaScript引擎。比如Strikingly的後端使用Ruby on Rails,只需要使用開源的react-rails gem就可以在Rails後端渲染前端React組件。
使用服務端渲染時要注意window和document這些瀏覽器才有的全域變數是不存在的。React組件提供這兩個lifecycle hook:componentDidMount和componentDidUpdate在伺服器不會被運行,只有在前端才會運行。使用伺服器渲染時如果要使用任何瀏覽器才有的變數需要把代碼放到這兩個lifecycle hook定義裡。
資料處理
React定義自己為MVC中的View。這讓前端開發人員從V開始去思考UI設計。但現在針對資料操作和擷取方式,社區裡還沒有一種公認的方法。這也是任何寫React應用時最難處理的地方。
【Flux】
對於M和C,Facebook提出了Flux的概念。Flux是一個專門為React設計的應用程式架構:應用程式由Dispatcher、Store和View組成,其中的View就是我們的React組件。Flux的核心是3所示的單向資料流動。
圖3 單向資料流動為Flux的核心
應用程式中的任何一次資料變化都作為Action發起,經過Dispatcher分發出去,被相關的Store接收到並整合,然後作為props和state提供給View(React組件)。當使用者在View上做了任何與資料相關的互動,View會發起新的Action,開啟一次新的資料變化周期。這種單向性使Flux在高層次上比傳統MVC架構和以Angular和Knockout為代表的雙向資料繫結容易理解得多,大大簡化了開發人員的思考和Debug過程。
在Facebook把Flux作為一種設計模式(而不是已經做好的架構)宣布之後,幾乎每個月出現一新的Flux庫,他們都有各自的特色,有的對伺服器渲染支援比較好,有的運用了更多函數式編程的概念。很多Flux庫更像是實驗,這有助於React生態的生長,但不可否認的是,未來會有大量Flux庫慢慢死去,而只有少數會存留下來或進行合并。
【GraphQL】
在構建大型前端應用時,前端和後端工程師通過API的方式進行合作。API也是雙方的協議。現在主流的方式是RESTful API,然而在實踐中,我們發現RESTful在一些真實生產環境的需求下不是很適用。往往我們需要構建自訂endpoint,而這違背了RESTful的設計理念。
舉個例子,我們想要顯示論壇文章、作者和對應的留言。我們分別要發出三個不同的請求。第二個請求依賴第一個請求結果返回的user_id,前端需要寫代碼協調請求之間的依賴。分別發出三個不同請求在移動端這種網路不穩定的環境下效果很不理想。
[js] view plaincopy
- GET /v1/posts/1
- {
- "id": 1,
- "title":"React.js in Strikingly",
- "user_id":2
- }
[js] view plaincopy
- GET /v1/users/2
- {
- "id":2,
- "name":"dfguo"
- }
[js] view plaincopy
- GET /v1/posts/1/comments
- [{
- "id":6,
- "name":"rechtar",
- "comment":"Thanks for sharing! I would love to see some examples on GraphQL."},{
- "id":9,
- "name":"tengbao",
- "comment":"I heard that you guys also use immutable.js. How did it help?"},{
- "id":12,
- "name":"syjstc",
- "comment":"Impressive work! Thanks guys!"
- },{
- "id":18,
- "name":"abeth86",
- "comment":"Thanks for the sharing!"
- }]
為解決這類問題,工程師會自訂一些endpoint。對於這個例子,我們可以建立一個/feeds的endpoint,集合了所有前端需要的結果:
[js] view plaincopy
- GET /v1/feeds/1
- {
- "id":1,
- "title":"React.js in Strikingly",
- "user":{
- "id":2,
- "name":"dfguo"
- },
- "comments":[
- {
- "id":6,
- "name":"rechtar",
- "comment":"Thanks for sharing! I would love to see some examples on GraphQL."
- }...
- ]
- }
但是我們在某些情境上可能只需要post和user,不想要comments。這時難道要再定義一個feeds_without_comments的endpoint?隨著需求的改變,自訂endpoint的方法往往使得API介面變得累贅,違背了RESTful的設計理念。而任何前端工程師需要的資料一旦要改變都需要後端工程師的配合,這降低了產品的迭代速度。
來自Facebook的GraphQL是我認為目前最接近完美的解決方案。後端工程師只需要定義可以被查詢的Type System,前端工程師就可以使用GraphQL自訂查詢。GraphQL查詢語句只需要形容需要返回的資料形狀:
{ post(id:1){ id, title, user{ id, name }, comments{ id, name, comment } } }
GraphQL伺服器就會返回正確的JSON格式:
[js] view plaincopy
- {
- "id":1,
- "title":"React.js in Strikingly",
- "user":{
- "id":2,
- "name":"dfguo"
- },
- "comments":[
- {
- "id":6,
- "name":"rechtar",
- "comment":"Thanks for sharing! I would love to see some examples on GraphQL.
- }...
- ]
- }
GraphQL也原生支援了API版本控制,讓你可以同時共存多個版本的用戶端(包括Web和Mobile)。這些都會減少用戶端工程師和後端工程師的耦合度,提高生產力。
今年7月剛推出了GraphQL的規範並開源了JavaScript GraphQL庫。然而要讓GraphQL成為主流,Facebook需要打造一個像React這樣的生態系統。要想在你自己的應用上用GraphQL還必須要有後端語言提供GraphQL庫的支援。比如Strikingly需要GraphQL Ruby庫。這不僅僅需要前端工程師。我們認為這將會比React生態系統更難建立(見圖4所示)。Facebook需要整個社區的參與才能達到。
圖4 GraphQL生態系統
【Relay】
Relay是Facebook提出的在React上應用GraphQL的方案。React的基礎單位是組件(Component),構建大型應用就是組合和嵌套組件。以組件為單位的設計模式是目前社區裡最認可的,這也是前端世界的趨勢之一。每個組件需要的資料也應該在組件內部定義。Relay讓組件可以自訂其所需要GraphQL資料格式,在組件執行個體化的時候再去GraphQL伺服器擷取資料。Relay也會自動構建嵌套組件的GraphQL查詢,這樣多個嵌套的組件只需要發一次請求。Relay將會在8月份開源。
【Immutability】
React社區接受了很多函數式編程的想法,其中受Clojure影響很深。對Immutable資料的使用就是來自Clojure社區。當年Om,這個用ClojureScript寫的React wrapper在速度上居然完虐原生JavaScript版本的React。這讓整個社區都震驚了。其中一個原因就是ClojureScript使用了Immutable資料。React社區裡也冒出了Immutable.js,這讓JavaScript裡也能使用Immutable資料,完美彌補了JavaScript在負責資料對象比較的先天性不足。Immutable.js也成為了構建大型React應用的必備。甚至有在討論是否把Immutable.js直接納入JavaScript語言中。我們認為小型應用不會遇到虛擬DOM的效能瓶頸,引入Immutable.js只會讓資料操作很累贅。
工具
工欲善其事,必先利其器。React的火爆得力於來自社區的工具,而React也推動了這些工具的進步。這裡我們想介紹幾個React社區裡比較受歡迎的工具。
【Webpack】
在React裡,由於需要用到JSX,使用Webpack或Browserify這類工具編譯代碼已經漸漸成為前端工程師工作流程的一部分。Webpack是一款強大的前端模組管理和打包工具(見圖5所示)。這裡列出它的一些特性:
- 同時支援CommonJS和AMD模組;
- 靈活和可擴充的Loader(載入器)機制,例如提供對JSX、ES6、Less的支援;
- 支援對CSS,圖片等其他資源進行打包;
- 可以基於配置和智能分析打包成多個檔案;
- 內建強大的Code Splitting功能可以拆分並動態載入包;
- 開發模式支援Hot Module Replacement模式,提高開發效率。
圖5 前端模組管理和打包工具Webpack
【Babel】
ECMAScript 6(ES6)規範在今年四月剛敲定,React社區基本全面擁抱ES6。但目前還有很多瀏覽器不支援ES6。使用像Webpack這樣的工具編譯代碼使得我們可以在開發時使用ES6(或者更新版本),在上線前編譯成ES5。編譯工具中最引人注意的是Babel。前身為ES6to5,Babel是目前社區最火的ES6編譯到ES5的代碼工具,Facebook團隊甚至已經決定轉用Babel而不再維護之前內部使用的jstranform。通過Loader機制,Webpack可以非常簡易地和Babel結合應用。
【React-hot-reload】
在開發任何大型前端應用過程中,我們常常會因為一些小錯誤就需要重新重新整理整個頁面。React-hot-reload嘗試解決這個問題,提高開發效率。他使用了Webpack的Hot Module Replacement功能,動態替換React組件的lifecycle hook定義,不用重新整理頁面也可以更新代碼變化。
【React Developer Tool】
這款Facebook官方推出的Chrome外掛程式可以讓你方便地在瀏覽器中直接查看React的組件結構。安裝後,在Chrome開發人員工具中會多出一個React Tab。介面就像DOM Inspector一樣,只不過是看React組件結構關係。是開發React應用不可多得的工具之一。
挑戰
React正在快速開拓著它的疆界,這意味在獲得新的喜悅的同時,我們也面臨著許多新的挑戰。現在圍繞著幾個大的議題,React社區仍沒有達成定論,每周甚至每天都有新的實驗項目在嘗試這些問題的解決。
【動畫】
一直以來大家都對動畫應該在React裡怎麼表達為狀態感到困惑。Cheng Lou的React Tween State是我們認為最符合React思維的做法。把位移存在State裡,然後通過JavaScript動態渲染新的位置。不過大家對該做法是否能達到滿意的速度一直持有保留態度。在今年ReactEurope的演講中,他為我們展現出了出色的效果和速度,非常值得一看。
在Strikingly,我們對於動畫則採取了比較實用主義的處理方式:我們定義了一些容器組件,比如〈JQFade/〉和〈JQSlide/〉,在其中調用jQuery的動畫方法來實現相應的Transition。這種方式在理論上並不完全符合React的精神,不過到現在為止還是能夠滿足我們需求的。
【Flux庫與Relay】
正如上文已經提到過的,目前Flux的各種實現可謂是百花齊放,其中還並沒有出現一個具有權威性的事實標準。Relay同樣也是剛剛孵化不久的新生概念——所有這些意味著雖然Flux+Relay會帶來生產力的飛升,要實際用上它們我們還要待以時日。
【CSS】
CSS是一個有趣的話題:似乎所有人都覺得當前的CSS有深刻的缺陷,但是對於怎麼解決這些缺陷大家的意見卻分成了兩派各不相讓:一派認為CSS“可以被修好”,並且致力於修好它,由此誕生了cssnext這樣的項目;另一派認為CSS從根本上作為誕生於一個古老時代的東西,已經不能適應大規模、組件化的現代開發流程,這一思想集中反映在Christopher Chedeau的演講《React: CSS in JS》中;在其中他提出了CSS的七個根本問題,然後指出在JavaScript中直接使用inline CSS可以幾乎“免費”地解決所有這些問題。在傳統的Web開發最佳實務中inline CSS一直是被壓制的反面實踐,現在我們卻能夠以一個全新的視角看待它,這也完美地例證了React真的是在給整個前端世界帶來根本性的推動。
總結
在不久前的JSConf 2015上赫門提出了前端的摩爾定理:前端每18月會難一倍。前端之所以變化這麼快,是因為我們現在面臨著前所未有的工程化挑戰。今天的前端複雜度跟幾年前完全不是一個等級。這也促使社區要找到在這種複雜度下能保持開發效率和開發體驗的工具和設計模式。React社區從其他領域(遊戲渲染、ClojureScript、函數式編程)偷師學藝,結合前端面臨的獨特問題,提出了一系列解決方案。React社區在各方面都推動著前端社區往前進。這對整個社區都是好事。我們也希望前端各個架構可以互相學習,共同推動整個社區的發展。
探索React生態圈