做者:yangchunwenhtml
React比較吸引個人地方在於其客戶端-服務端同構特性,服務端-客戶端可複用組件,本文來簡單介紹下這一架構思想。node
出於篇幅緣由,本文不會介紹React基礎,因此,若是你還不清楚React的state/props/生存週期等基本概念,建議先學習相關文檔react
先來回顧一下React如何寫一個組件。好比要作一個下面的表格:
webpack
能夠這樣寫:
先建立一個表格類。
Table.jsgit
var React = require('react'); var DOM = React.DOM; var table = DOM.table, tr = DOM.tr, td = DOM.td; module.exports = React.createClass({ render: function () { return table({ children: this.props.datas.map(function (data) { return tr(null, td(null, data.name), td(null, data.age), td(null, data.gender) ); }) }); } });
假設已經有了咱們要的表格的結構化數據。
datas.js:github
// 三行數據,分別包括名字、年齡、性別 module.exports = [ { 'name': 'foo', 'age': 23, 'gender': 'male' }, { 'name': 'bar', 'age': 25, 'gender': 'female' }, { 'name': 'alice', 'age': 34, 'gender': 'male' } ];
有了表格類和相應的數據以後,就能夠調用並渲染這個表格了。
render-client.jsweb
var React = require('react'); var ReactDOM = require('react-dom'); // table類 var Table = require('./Table'); // table實例 var table = React.createFactory(Table); // 數據源 var datas = require('./datas'); // render方法把react實例渲染到頁面中 https://facebook.github.io/react/docs/top-level-api.html#reactdom ReactDOM.render( table({datas: datas}), document.body );
咱們把React基礎庫
、Table.js
、datas.js
、render-client.js
等打包成pack.js
,引用到頁面中:ajax
<!doctype html> <html> <head> <title>react</title> </head> <body> </body> <script src="pack.js"></script> </html>'
這樣頁面即可按數據結構渲染出一個表格來數據庫
這裏 pack.js 的具體打包工具能夠是grunt/gulp/webpack/browerify等,打包方法不在這裏贅述編程
這個例子的關鍵點是使用props
來傳遞單向數據流。例如,經過遍歷從`props
傳來的數據`datas```生成表格的每一行數據:
this.props.datas.map...
組件的每一次變動(好比有新增數據),都會調用組件內部的render方法,更改其DOM結構。上面這個例子中,當給datas
push新數據時,react會自動爲頁面中的表格新增數據行。
上面的例子中建立的Table
組件,出於性能、SEO等因素考慮,咱們會考慮在服務端直接生成HTML結構,這樣就能夠在瀏覽器端直接渲染DOM了。
這時候,咱們的Table
組件,就能夠同時在客戶端和服務端使用了。
只不過與瀏覽器端使用ReactDOM.render
指定組件的渲染目標不一樣,在服務器中渲染,使用的是ReactDOMServer這個模塊,它有兩個生成HTML字符串的方法:
關於這兩個方法的區別,我想放到後面再來解釋,由於跟後面介紹的內容頗有關係。
有了這兩個方法,咱們來建立一個在服務端nodejs環境運行的文件,使之能夠直接在服務端生成表格的HTML結構。
render-server.js:
var React = require('react'); // 與客戶端require('react-dom')略有不一樣 var React = require('react'); // 與客戶端require('react-dom')略有不一樣 var ReactDOMServer = require('react-dom/server'); // table類 var Table = require('./Table'); // table實例 var table = React.createFactory(Table); module.exports = function () { return ReactDOMServer.renderToString(table(datas)); };
上面這段代碼複用了同一個Table
組件,生成瀏覽器能夠直接渲染的HTML結構,下面咱們經過改改nodejs的官方Hello World來作一個真實的頁面。
server.js :
var makeTable = require('./render-server'); var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/html'}); var table = makeTable(); var html = '<!doctype html>\n\ <html>\ <head>\ <title>react server render</title>\ </head>\ <body>' + table + '</body>\ </html>'; res.end(html); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
這時候運行node server.js
就能看到,不實用js,達到了一樣的表格效果,這裏我使用了同一個Table.js
,完成客戶端及服務端的同構,一份代碼,兩處使用。
這裏咱們經過查看頁面的HTML源碼,發現表格的DOM中帶了一些數據:
data-reactid
/ data-react-checksum
都是些啥?這裏一樣先留點懸念,後面再解釋。
上面的這個例子,經過在服務端調用同一個React組件,達到了一樣的界面效果,可是有人可能會不開心了:貌似有點弱啊!
上面的例子有兩個明顯的問題:
datas.js 數據源是寫死的,不符合大部分真實生產環境
服務端生成HTML結構有時候並不完善,有時候不借助js是不行的。好比當咱們的表格須要輪詢服務器的數據接口,實現表格數據與服務器同步的時候,怎麼實現一個組件兩端使用。
爲了解決這個問題,咱們的Table組件須要變得更復雜。
假設咱們的表格數據每過一段時間要和服務端同步,在瀏覽器端,咱們必須藉助ajax
,React官方給咱們指明瞭這類需求的方向,經過componentDidMount
這一輩子存週期方法來拉取數據。
componentDidMount
方法,我我的把它比喻成一個「善後」的方法,就是在React把基本的HTML結構掛載到DOM中後,再經過它來作一些善後的事情,例如拉取數據更新DOM等等。
因而咱們改一下咱們的`Table
組件,去掉假數據datas.js
,在`componentDidMount```中調用咱們封裝好的抓取數據方法,每三秒去服務器抓取一次數據並更新到頁面中。
Table.js:
var React = require('react'); var ReactDOM = require('react-dom'); var DOM = React.DOM; var table = DOM.table, tr = DOM.tr, td = DOM.td; var Data = require('./data'); module.exports = React.createClass({ render: function () { return table({ children: this.props.datas.map(function (data) { return tr(null, td(null, data.name), td(null, data.age), td(null, data.gender) ); }) }); }, componentDidMount: function () { setInterval(function () { Data.fetch('http://datas.url.com').then(function (datas) { this.setProps({ datas: datas }); }); }, 3000) } });
這裏假設咱們已經封裝了一個拉取數據的
Data.fetch
方法,例如Data.fetch = jQuery.ajax
到這一步,咱們實現了客戶端的每3秒自動更新表格數據。那麼上面這個Table組件是否是能夠直接複用到服務端,實現數據拉取呢,很差意思,答案是「不」。
React的奇葩之一,就是其組件有「生存週期」這一說法,在組件的生命的不一樣時期,例如異步數據更新,DOM銷燬等等過程,都會調用不一樣的生命週期方法。
然而服務端狀況不一樣,對服務端來講,它要作的事情即是:去數據庫拉取數據 -> 根據數據生成HTML -> 吐給客戶端。這是一個固定的過程,拉取數據和生成HTML過程是不可打亂順序的,不存在先把內容吐給客戶端,再拉取數據這樣的異步過程。
因此,componentDidMount
這樣的「善後」方法,React在服務器渲染組件的時候,就不適用了。
並且我還要告訴你,componentDidMount
這個方法,在服務端確實永遠都不會執行!
看到這裏,你可能要想,這步坑爹嗎!搞了半天,這個東西只能在客戶端用,說好的同構呢!
別急,拉取數據,咱們須要另外的方法。
React中能夠經過statics
定義「靜態方法」,學過面向對象編程的同窗,天然懂statics
方法的意思,沒學過的,拉出去打三十大板。
咱們再來改一下Table
組件,把拉取數據的Data.fetch
邏輯放到這裏來。
Table.js:
var React = require('react'); var DOM = React.DOM; var table = DOM.table, tr = DOM.tr, td = DOM.td; var Data = require('./data'); module.exports = React.createClass({ statics: { fetchData: function (callback) { Data.fetch().then(function (datas) { callback.call(null, datas); }); } }, render: function () { return table({ children: this.props.datas.map(function (data) { return tr(null, td(null, data.name), td(null, data.age), td(null, data.gender) ); }) }); }, componentDidMount: function () { setInterval(function () { // 組件內部調用statics方法時,使用this.constructor.xxx... this.constructor.fetchData(function (datas) { this.setProps({ datas: datas }); }); }, 3000); } });
很是重要:Table組件能在客戶端和服務端複用fetchData方法拉取數據的關鍵在於,
Data.fetch
必須在客戶端和服務端有不一樣的實現!例如在客戶端調用Data.fetch
時,是發起ajax請求,而在服務端調用Data.fetch
時,有多是經過UDP協議從其餘數據服務器獲取數據、查詢數據庫等實現
因爲服務端React不會調用componentDidMount
,須要改一下服務端渲染的文件,一樣再也不經過datas.js獲取數據,而是調用Table的靜態方法fetchData
,獲取數據後,再傳遞給服務端渲染方法renderToString
,獲取數據在實際生產環境中是個異步過程,因此咱們的代碼也須要是異步的:
render-server.js:
var React = require('react'); var ReactDOMServer = require('react-dom/server'); // table類 var Table = require('./Table'); // table實例 var table = React.createFactory(Table); module.exports = function (callback) { Table.fetchData(function (datas) { var html = ReactDOMServer.renderToString(table({datas: datas})); callback.call(null, html); }); };
這時候,咱們的Table
組件已經實現了每3秒更新一次數據,因此,咱們既須要在服務端調用React初始html數據,還須要在客戶端調用React實時更新,因此須要在頁面中引入咱們打包後的js。
server.js
var makeTable = require('./render-server'); var http = require('http'); http.createServer(function (req, res) { if (req.url === '/') { res.writeHead(200, {'Content-Type': 'text/html'}); makeTable(function (table) { var html = '<!doctype html>\n\ <html>\ <head>\ <title>react server render</title>\ </head>\ <body>' + table + '<script src="pack.js"></script>\ </body>\ </html>'; res.end(html); }); } else { res.statusCode = 404; res.end(); } }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
經過上面的改動,咱們在服務端獲取表格數據,生成HTML供瀏覽器直接渲染;頁面渲染後,Table組件每隔3秒會經過ajax獲取新的表格數據,有數據更新的話,會直接更新到頁面DOM中。
還記得前面的問題麼?
ReactDOMServer.renderToString
和 ReactDOMServer.renderToStaticMarkup
有什麼不一樣?服務端生成的data-react-checksum
是幹嗎使的?
咱們想想,就算服務端沒有初始化HTML數據,僅僅依靠客戶端的React也徹底能夠實現渲染咱們的表格,那服務端生成了HTML數據,會不會在客戶端React執行的時候被從新渲染呢?咱們服務端辛辛苦苦生成的東西,被客戶端無情地覆蓋了?
固然不會!React在服務端渲染的時候,會爲組件生成相應的校驗和(checksum),這樣客戶端React在處理同一個組件的時候,會複用服務端已生成的初始DOM,增量更新,這就是data-react-checksum
的做用。
ReactDOMServer.renderToString
和 ReactDOMServer.renderToStaticMarkup
的區別在這個時候就很好解釋了,前者會爲組件生成checksum,然後者不會,後者僅僅生成HTML結構數據。
因此,只有你不想在客戶端-服務端同時操做同一個組件的時候,方可以使用renderToStaticMarkup
。