React 同構思想

做者:yangchunwenhtml

React比較吸引個人地方在於其客戶端-服務端同構特性,服務端-客戶端可複用組件,本文來簡單介紹下這一架構思想。node

出於篇幅緣由,本文不會介紹React基礎,因此,若是你還不清楚React的state/props/生存週期等基本概念,建議先學習相關文檔react

客戶端React

先來回顧一下React如何寫一個組件。好比要作一個下面的表格:
tablewebpack

能夠這樣寫:
先建立一個表格類。
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.jsgithub

// 三行數據,分別包括名字、年齡、性別
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.jsdatas.jsrender-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會自動爲頁面中的表格新增數據行。

服務端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中帶了一些數據:
html

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中。

checksum的做用

還記得前面的問題麼?

ReactDOMServer.renderToStringReactDOMServer.renderToStaticMarkup 有什麼不一樣?服務端生成的data-react-checksum是幹嗎使的?

咱們想想,就算服務端沒有初始化HTML數據,僅僅依靠客戶端的React也徹底能夠實現渲染咱們的表格,那服務端生成了HTML數據,會不會在客戶端React執行的時候被從新渲染呢?咱們服務端辛辛苦苦生成的東西,被客戶端無情地覆蓋了?

固然不會!React在服務端渲染的時候,會爲組件生成相應的校驗和(checksum),這樣客戶端React在處理同一個組件的時候,會複用服務端已生成的初始DOM,增量更新,這就是data-react-checksum的做用。

ReactDOMServer.renderToStringReactDOMServer.renderToStaticMarkup 的區別在這個時候就很好解釋了,前者會爲組件生成checksum,然後者不會,後者僅僅生成HTML結構數據。

因此,只有你不想在客戶端-服務端同時操做同一個組件的時候,方可以使用renderToStaticMarkup

原文連接:http://ivweb.io/topic/5636466d09e01a534b461ec3

相關文章
相關標籤/搜索