做者: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
。