#### 1.我们服务端渲染数据从何而来 ##### 1.1 如何写出同构的组件 服务端生成HTML结构有时候并不完善,有时候不借助js是不行的。比如当我们的组件需要轮询服务器的数据接口,实现数据与服务器同步的时候就显得很重要。其实这个获取数据的过程可以是数据库获取,也可以是从其他的[反向代理服务器](https://github.com/liangklfangl/react-universal-bucket)来获取。对于客户端来说,我们可以通过ajax请求来完成,只要将ajax请求放到componentDidMount方法中来完成就可以。而之所以放在该方法中有两个原因,第一个是为了保证此时DOM已经挂载到页面中;另一个原因是在该方法中调用setState会导致组件重新渲染(具体你可以查看[这个文章](https://segmentfault.com/q/1010000008133309/a-1020000008135702))。而对于服务端来说, 一方面它要做的事情便是:*去数据库或者反向代理服务器拉取数据 -> 根据数据生成HTML -> 吐给客户端*。这是一个固定的过程,拉取数据和生成HTML过程是不可打乱顺序的,不存在先把内容吐给客户端,再拉取数据这样的异步过程。所以,componentDidMount在服务器渲染组件的时候,就不适用了(*因为render方法已经调用,但是componentDidMount还没有执行,所以渲染得到的是没有数据的组件。原因在于生命周期方法componentDidMount在render之后才会调用*)。 另一方面,componentDidMount这个方法,在服务端确实永远都不会执行!因此我们要采用和客户端渲染完全不一致的方法来解决渲染之前数据不存在问题。关于服务端渲染和客户端渲染的区别你可以查看[Node直出理论与实践总结](https://github.com/joeyguo/blog/issues/8) ```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 // 客户端在componentDidMount中获取数据,并调用setState修改状态要求 // 组件重新渲染 this.constructor.fetchData(function (datas) { this.setProps({ datas: datas }); }); }, 3000); } }); ``` 其中服务器端的处理逻辑render-server.js如下: ```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) { //在客户端调用Data.fetch时,是发起ajax请求,而在服务端调用Data.fetch时, //有可能是通过UDP协议从其他数据服务器获取数据、查询数据库等实现 Table.fetchData(function (datas) { var html = ReactDOMServer.renderToString(table({datas: datas})); callback.call(null, html); }); }; ``` 下面是服务器的逻辑server.js: ```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'}); //先访问数据库或者反代理服务器来获取到数据,并注册回调,将含有数据的html结构返回给客户端,此处只是渲染一个组件,否则需要renderProps.components.forEach来遍历所有的组件获取数据 //http://www.toutiao.com/i6284121573897011714/ makeTable(function (table) { var html = '\n\ \
\如果data-react-checksum相同则不重新render,省略创建DOM和挂载DOM的过程,接着触发 componentDidMount 等事件来处理服务端上的未尽事宜(事件绑定等),从而加快了交互时间;不同时,组件在客户端上被重新挂载 render。ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 的区别在这个时候就很好解释了,前者会为组件生成checksum,而后者不会,后者仅仅生成HTML结构数据。所以,只有你不想在客户端-服务端同时操作同一个组件的时候,方可使用renderToStaticMarkup。注意:上面使用了statics块,该写法只在createClass中可用,你可以使用下面的写法: ```js //组件内的写法 class Component extends React.Component { static propTypes = { ... } static someMethod(){ } } ``` 在组件外面你可以按照如下写法: ```js class Component extends React.Component { .... } Component.propTypes = {...} Component.someMethod = function(){....} ``` 具体你可以[查看这里](https://stackoverflow.com/questions/29433130/react-statics-with-es6-classes)。关于服务端渲染经常会出现下面的warning,大多数情况下是因为在返回 HTML 的时候没有将服务端上的数据一同返回,或者是返回的数据格式不对导致
Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generatted on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Insted, figure out why the markup being generated is different on the client and server#### 2.如何区分客户端与服务端代码 ##### 2.1 添加客户端代码到服务端渲染的html字符串 通过[这个例子](https://github.com/liangklfangl/react-universal-bucket)我们知道,将webpack-isomorphic-tools这个插件添加到webpack的plugin中: ```js module.exports = { entry:{ 'main': [ 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr', // "bootstrap-webpack!./src/theme/bootstrap.config.js", "bootstrap-loader", //确保安装bootstrap3,bootstrap4不支持less './src/client.js' ] }, output: { path: assetsPath, filename: '[name]-[hash].js', chunkFilename: '[name]-[chunkhash].js', publicPath: 'http://' + host + ':' + port + '/dist/' //表示要访问我们客户端打包好的资源必须在前面加上的前缀,也就是虚拟路径 }, plugins:[ new webpack.DefinePlugin({ __CLIENT__: true, __SERVER__: false, __DEVELOPMENT__: true, __DEVTOOLS__: true //, }), webpackIsomorphicToolsPlugin.development() //在webpack的development模式下一定更要调用它支持asset hold reloading! //https://github.com/liangklfang/webpack-isomorphic-tools ] } ``` 此时我们client.js会被打包到相应的文件路径下,然后在我们的模版中,只要将这个打包好的script文件添加到html返回给客户端就可以了。下面是遍历我们的webpack-assets.json来获取到我们所有的产生的资源,然后添加到html模板中返回的逻辑: ```js export default class Html extends Component { static propTypes = { assets: PropTypes.object, component: PropTypes.node, store: PropTypes.object }; render() { const {assets, component, store} = this.props; const content = component ? renderToString(component) : ''; //如果有组件component传递过来,那么我们直接调用renderToString const head = Helmet.rewind(); return ( {head.base.toComponent()} {head.title.toComponent()} {head.meta.toComponent()} {head.link.toComponent()} {head.script.toComponent()} {/* styles (will be present only in production with webpack extract text plugin) styles属性只有在生产模式下才会存在,此时通过link来添加。便于缓存 */} {Object.keys(assets.styles).map((style, key) => )} {/* assets.styles如果开发模式下,那么肯定是空,那么我们直接采用内联的方式来插入即可。此时我们的css没有单独抽取出来,也就是没有ExtractTextWebpackPlugin,打包到js中从而内联进来 */} {/* (will be present only in development mode) */} {/* outputs a tag with all bootstrap styles + App.scss + it could be CurrentPage.scss. */} {/* can smoothen the initial style flash (flicker) on page load in development mode. */} {/* ideally one could also include here the style for the current page (Home.scss, About.scss, etc) */} {/*将组件renderToString后放在id为content的div内部*/} {/*将store.getState序列化后放在window.__data上,让客户端代码可以拿到*/} {/*将我们的main.js,来自于客户端打包并放在特定文件夹下的资源放在页面中, 这就成了客户端自己的js资源了 */} ); } } ``` 所以说下面的div#content中是服务端渲染后得到的html字符串,并被原样返回给客户端。这样的话,对于服务端的任务就完成了 ```html ``` 而我们的下面的script标签的内容就是我们的客户端代码打包后的结果: ```js ``` 此时客户端和服务端的逻辑都已经完成了,客户端可以继续接收用户操作而发送ajax请求更新组件状态。 ##### 2.2 如何使得服务端和客户端发起请求的逻辑通用 一个好的用法在于使用[isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) ##### 2.3 immutable数据在同构中的注意事项 首先在服务端返回的时候必须将store.getState得到的结果[序列化](https://github.com/yahoo/serialize-javascript),而且此时如果store返回的某一个部分state是immutbale的,那么客户端要重新通过这部分state数据来创建新的immutable对象(如下面的例子中我们的recipeGrid和connect是immutable的): ```js ``` 对于客户端来说,我们必须将从服务端注入到HTML上的state数据转成 immutable对象,并将该对象作为initialState来创建store: ```js const data = window.__data; //其中data是服务端返回的store.getState的值,也就是store的当前状态 if (data) { data.recipeGrid = Immutable.fromJS(data.recipeGrid); //这里必须设置,否则报错说:paginator.equals is not a function data.connect = Immutable.fromJS(data.connect); //可以使用https://github.com/liangklfang/redux-immutablejs } const store = finalCreateStore(reducer, data); ``` ##### 2.4 服务端server不支持ES6的兼容 如果你想在服务端使用import等ES6的语法的话,你可以采用下面的方式,首先在项目的根目录下配置.babelrc文件,内容如下: ```js { "presets": ["react", "es2015", "stage-0"], "plugins": [ "transform-runtime", "add-module-exports", "transform-decorators-legacy", "transform-react-display-name" ] } ``` 然后配置一个单独的文件server.babel.js: ```js const fs = require("fs"); const babelrc = fs.readFileSync("./.babelrc"); let config ; try{ config = JSON.parse(babelrc); }catch(err){ console.error("你的.babelrc文件有误,请仔细检查"); console.error(err); } //你可以指定ignore配置来忽略某些文件。 //https://github.com/babel/babel/tree/master/packages/babel-register require("babel-register")(config); //require("babel-register")会导致以后所有的.es6,.es,.js,.jsx的文件都会被babel处理 ``` 最后我们添加我们的server.js,内容如下(直接node server.js,而真正的逻辑放在../src/server中): ```js #!/usr/bin/env node require('../server.babel'); // babel registration (runtime transpilation for node) var path = require('path'); var rootDir = path.resolve(__dirname, '..'); global.__CLIENT__ = false; global.__SERVER__ = true; global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; if (__DEVELOPMENT__) { //服务端代码热加载 if (!require('piping')({ hook: true, ignore: /(\/\.|~$|\.json|\.scss$)/i })) { return; } } // https://github.com/halt-hammerzeit/webpack-isomorphic-tools var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools-config')) .development(__DEVELOPMENT__) .server(rootDir, function() { //rootDir必须和webpack的context一致,调用这个方法服务器就可以直接require任何资源了 //这个路径用于获取webpack-assets.json文件,这个是webpack输出的 // webpack-isomorphic-tools is all set now. // here goes all your web application code: // (it must reside in a separate *.js file // in order for the whole thing to work) // 此时webpack-isomorphic-tools已经注册好了,这里可以写你的web应用的代码,而且这些代码必须在一个独立的文件中 require('../src/server'); }); ``` 经过上面的babel-register的处理,此时你的../src/server.js中可以使用任意ES6的代码了。 ##### 2.5 服务端代码单独使用webpack打包 如果对于服务端的代码要单独打包,那么必须进行下面的设置: ```js target: "node" ``` 你可以[参考这里](https://github.com/liangklfang/universal-react-demo)。 ##### 2.6 服务端渲染之忽略css/less/scss文件 在2.4中我们使用了babel-register帮助服务端识别特殊的js语法,但对less/css文件无能为力,庆幸的是,在一般情况下,服务端渲染不需要样式文件的参与,css文件只要引入到HTML文件中即可,因此,可以通过配置项,忽略所有 css/less 文件: ```js require("babel-register")({ //默认情况ignore是node_modules表示node_modules下的所有文件的require不会进行处理 //这里明确指定css/less不经过babel处理 ignore: /(.css|.less)$/, }); ``` 具体内容你可以查看[babel-register文档](https://github.com/babel/babel/tree/master/packages/babel-register)。你可以传递其指定的所有的其他选项,包括plugins和presets。但是有一点要注意,就是距离我们源文件的最近一个*.babelrc*始终会起作用,同时其优先级也要比你在此配置的选项优先级高。此时我们忽略了样式文件的解析并不会导致客户端对组件再次渲染,因为我们的checksum和具体的css/less/scss文件无关,只是和组件render的结果有关。 ##### 2.7 使用webpack-isomorphic-tools识别css/less/scss文件 通过 babel-register 能够使用babel解决jsx语法问题,对 css/less 只能进行忽略,但在使用了CSS Modules 的情况下,服务端必须能够解析 less文件,才能得到转换后的类名,否者服务端渲染出的HTML结构和打包生成的客户端 css 文件中,类名无法对应。其原因在于:我们在服务端使用了CSS Module的情况下必须采用如下的方式来完成类名设置: ```js const React = require("react"); const styles = require("./index.less"); class Test extends React.Component{ render(){ return ( //如果不是css module,那么可能是这种情况:className="banner"
服务端渲染,仅仅是渲染静态的页面内容而已,并不做任何的事件绑定。所有的事件绑定都是在客户端进行的。当然,在结合了redux的情况下,你可以参考我[这个例子](https://github.com/liangklfangl/react-universal-bucket)来看看如何进行了同构。 参考资料: [React同构思想](http://imweb.io/topic/5636466d09e01a534b461ec3) [React数据获取为什么一定要在componentDidMount里面调用?](https://segmentfault.com/q/1010000008133309/a-1020000008135702) [ReactJS 生命周期、数据流与事件](http://www.codeceo.com/article/reactjs-life-circle-event.html) [React statics with ES6 classes](https://stackoverflow.com/questions/29433130/react-statics-with-es6-classes) [React同构直出优化总结](http://www.tuicool.com/articles/qYbEJ3v) [腾讯新闻React同构直出优化实践](http://www.tuicool.com/articles/Y7b6zmR) [Node直出理论与实践总结](https://github.com/joeyguo/blog/issues/8) [React+Redux 同构应用开发](http://www.aliued.com/?p=3077) [ReactJS 服务端同构实践「QQ音乐web团队」](http://www.toutiao.com/i6284121573897011714/) [代码拆分 - 使用 require.ensure](http://www.css88.com/doc/webpack2/guides/code-splitting-require/) [性能优化三部曲之三——Node直出让你的网页秒开 #6](https://github.com/lcxfs1991/blog/issues/6) [React koa2 同构应用实践](https://www.zeroling.com/react-koa2-isomorphic-practise/?utm_source=tuicool&utm_medium=referral) [从零开始React服务器渲染](http://www.tuicool.com/articles/ziIzMrq) [React直出实现与原理](http://www.tuicool.com/articles/YrmYZf6) [redux+react同构实例](https://github.com/liangklfangl/react-universal-bucket) [React 同构开发(一)](http://www.cnblogs.com/bingooo/p/5724354.html) [React 同构开发(二)](http://www.cnblogs.com/bingooo/p/5827535.html)