標籤:
React比較吸引我的地方在於其用戶端-服務端同構特性,服務端-用戶端可複用組件,本文來簡單介紹下這一架構思想。
出於篇幅原因,本文不會介紹React基礎,所以,如果你還不清楚React的state/props/生存周期等基本概念,建議先學習相關文檔
用戶端React
先來回顧一下React如何寫一個組件。比如要做一個下面的表格:
可以這樣寫: 先建立一個表格類。 Table.js
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:
// 三行資料,分別包括名字、年齡、性別module.exports = [ { ‘name‘: ‘foo‘, ‘age‘: 23, ‘gender‘: ‘male‘ }, { ‘name‘: ‘bar‘, ‘age‘: 25, ‘gender‘: ‘female‘ }, { ‘name‘: ‘alice‘, ‘age‘: 34, ‘gender‘: ‘male‘ }];
有了表格類和相應的資料之後,就可以調用並渲染這個表格了。 render-client.js
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#reactdomReactDOM.render( table({datas: datas}), document.body);
我們把React基礎庫
、Table.js
、datas.js
、render-client.js
等打包成pack.js
,引用到頁面中:
<!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會自動為頁面中的表格新增資料行。
服務端React
上面的例子中建立的Table
組件,出於效能、SEO等因素考慮,我們會考慮在服務端直接產生HTML結構,這樣就可以在瀏覽器端直接渲染DOM了。
這時候,我們的Table
組件,就可以同時在用戶端和服務端使用了。
只不過與瀏覽器端使用ReactDOM.render
指定組件的渲染目標不同,在伺服器中渲染,使用的是ReactDOMServer這個模組,它有兩個產生HTML字串的方法:
- renderToString
- renderToStaticMarkup
關於這兩個方法的區別,我想放到後面再來解釋,因為跟後面介紹的內容很有關係。
有了這兩個方法,我們來建立一個在服務端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組件,達到了同樣的介面效果,但是有人可能會不開心了:貌似有點弱啊!
上面的例子有兩個明顯的問題:
為瞭解決這個問題,我們的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中。
checksum的作用
還記得前面的問題嗎?
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
。
[轉] React同構思想