微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

从零开始React服务器渲染

2017-01-22 21:14 前端大全

(点击上方公众号,可快速关注)


作者:AlloyTeam

www.alloyteam.com/2017/01/react-from-scratch-server-render/

如有好文章投稿,请点击 → 这里了解详情


一.前言


当我们选择使用Node+React的技术栈开发Web时,React提供了一种优雅的方式实现服务器渲染。使用React实现服务器渲染有以下好处:


1.利于SEO:React服务器渲染的方案使你的页面在一开始就有一个HTML DOM结构,方便Google等搜索引擎的爬虫能爬到网页的内容。


2.提高首屏渲染的速度:服务器直接返回一个填满数据的HTML,而不是在请求了HTML后还需要异步请求首屏数据。


3.前后端都可以使用js


二.神奇的renderToString和renderToStaticMarkup


有两个神奇的React API都可以实现React服务器渲染:renderToString和renderToStaticMarkup。renderToString和renderToStaticMarkup的主要作用都是将React Component转化为HTML的字符串。这两个函数都属于react-dom(react-dom/server)包,都接受一个React Component参数,返回一个String。


也许你会奇怪为什么会有两个用于服务器渲染的函数,其实这两个函数是有区别的:


1.renderToString:将React Component转化为HTML字符串,生成的HTML的DOM会带有额外属性:各个DOM会有data-react-id属性,第一个DOM会有data-checksum属性。


2.renderToStaticMarkup:同样是将React Component转化为HTML字符串,但是生成HTML的DOM不会有额外属性,从而节省HTML字符串的大小。


下面是一个在服务器端使用renderToStaticMarkup渲染静态页面的例子:


npm包安装:


npm -S install express react react-dom


server.js:


var express = require('express');

var app = express();

 

var React = require('react'),

ReactDOMServer = require('react-dom/server');

 

var App = React.createFactory(require('./App'));

 

app.get('/', function(req, res) {

  var html = ReactDOMServer.renderToStaticMarkup(

    React.DOM.body(

    null,

    React.DOM.div({id: 'root',

   dangerouslySetInnerHTML: {

      __html: ReactDOMServer.renderToStaticMarkup(App())

   }

 })

 )

);

 

  res.end(html);

});

 

app.listen(3000, function() {

  console.log('running on port ' + 3000);

});


App.js:


var React = require('react'),

    DOM = React.DOM, div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li

 

module.exports = React.createClass({

  getInitialState: function() {

   return {

     isSayBye: false

   }

  },

  handleClick: function() {

   this.setState({

     isSayBye: !this.state.isSayBye

   })

  },

  render: function() {

    var content = this.state.isSayBye ? 'Bye' : 'Hello World';

    return div(null,

      div(null, content),

      button({onClick: this.handleClick}, 'switch')

    );

  }

})


运行:


node server.js


结果:



三.动态的React组件


上例的页面中,点击“switch”按钮是没有反应的,这是因为这个页面只是一个静态的HTML页面,没有在客户端渲染React组件并初始化React实例。只有在初始化React实例后,才能更新组件的state和props,初始化React的事件系统,执行虚拟DOM的重新渲染机制,让React组件真正“动”起来。


或许你会奇怪,服务器端已经渲染了一次React组件,如果在客户端中再渲染一次React组件,会不会渲染两次React组件。答案是不会的。秘诀在于data-react-checksum属性:


上文有说过,如果使用renderToString渲染组件,会在组件的第一个DOM带有data-react-checksum属性,这个属性是通过adler32算法算出来:如果两个组件有相同的props和DOM结构时,adler32算法算出的checksum值会一样,有点类似于哈希算法。


当客户端渲染React组件时,首先计算出组件的checksum值,然后检索HTML DOM看看是否存在数值相同的data-react-checksum属性,如果存在,则组件只会渲染一次,如果不存在,则会抛出一个warning异常。也就是说,当服务器端和客户端渲染具有相同的props和相同DOM结构的组件时,该React组件只会渲染一次。


在服务器端使用renderToStaticMarkup渲染的组件不会带有data-react-checksum属性,此时客户端会重新渲染组件,覆盖掉服务器端的组件。因此,当页面不是渲染一个静态的页面时,最好还是使用renderToString方法。


上述的客户端渲染React组件的流程图如下:



四.一个完整的例子


下面使用React服务器渲染实现一个简单的计数器。为了简单,本例中不使用redux、react-router框架,尽量排除各种没必要的东西。


项目目录如下:



npm包安装:


npm install -S express react react-dom jsx-loader


webpack.config.js:webpack配置文件,作用是在客户端中可以使用代码模块化和jsx形式的组件编写方式:


var path = require('path');

 

var assetsPath = path.join(__dirname, "public", "assets");

var serverPath = path.join(__dirname, "server");

 

module.exports = [

{

name: "browser",

entry: './app/entry.js',

output: {

path: assetsPath,

filename: 'entry.generator.js'

},

module: {

        loaders: [

            { test: /\.js/, loader: "jsx-loader" }

        ]

    }

 

},

{

name: "server-side rending",

entry: './server/page.js',

output: {

path: serverPath,

filename: "page.generator.js",

// 使用page.generator.js的是nodejs,所以需要将

// webpack模块转化为CMD模块

library: 'page',

libraryTarget: 'commonjs'

},

module: {

loaders: [

{ test: /\.js$/, loader: 'jsx-loader' }

]

}

}

]


app/App.js:根组件 (一个简单的计数器组件),在客户端和服务器端都需要引入使用


var React = require('react');

 

var App = React.createClass({

getInitialState: function() {

        return {

            count: this.props.initialCount

        };

    },

 

    _increment: function() {

        this.setState({ count: this.state.count + 1 });

    },

 

render: function() {

return (

<div>

<span>the count is: </span>

<span onClick={this._increment}>{this.state.count}</span>

</div>

)

}

})

 

module.exports = App;


server/index.js:服务器入口文件:


var express = require('express');

var path = require('path');

 

var page = require("./page.generator.js").page;

 

var app = express();

var port = 8082;

 

app.use(express.static(path.join(__dirname, '..', 'public')));

 

app.get('/', function(req, res) {

var props = {

initialCount: 9

};

var html = page(props);

res.end(html);

});

 

app.listen(port, function() {

console.log('Listening on port %d', port);

});


server/page.js:暴露一个根组件转化为字符串的方法


var React = require('react');

var ReactDOMServer = require("react-dom/server");

 

var App = require('../app/App');

 

var ReactDOM = require('react-dom');

 

 

module.exports = function(props) {

 

var content = ReactDOMServer.renderToString(

<App initialCount={props.initialCount}></App>

);

 

var propsScript = 'var APP_PROPS = ' + JSON.stringify(props);

 

var html = ReactDOMServer.renderToStaticMarkup(

<html>

<head>

</head>

<body>

<div id="root" dangerouslySetInnerHTML={

{__html: content}

} />

<script dangerouslySetInnerHTML={

{__html: propsScript}

}></script>

<script src={"assets/entry.generator.js"}></script>

</body>

</html>

);

 

return html;

}


为了让服务器端和客户端的props一致,将一个服务器生成的首屏props赋给客户端的全局变量APP_PROPS,在客户端初始化根组件时使用这个APP_PROPS根组件的props。


app/entry.js:客户端入口文件,用于在客户端渲染根组件,别忘了使用在服务器端写入的APP_PROPS初始化根组件的props


var React = require('react'),

ReactDOM = require('react-dom'),

App = require('./App');

 

var APP_PROPS = window.APP_PROPS || {};

 

ReactDOM.render(

<App initialCount={APP_PROPS.initialCount}/>,

document.getElementById('root')

);


源代码放在github上,懒得复制粘贴搭建项目的同学可以猛戳这里(https://github.com/RockyRen/react-webpack-server-render-example)


github上还有其他的服务器渲染的例子,有兴趣的同学可以参考参考:


1.无webpack无jsx版本(https://github.com/mhart/react-server-example)


2.使用webpack版本(https://github.com/webpack/react-webpack-server-side-example)


参考文章:


  1. Rendering React Components on the Server

  2. 一看就懂的 React Server Rendering(Isomorphic JavaScript)入門教學

  3. Clientside react-script overrides serverside rendered props

  4. React直出实现与原理

  5. React Server Side Rendering 解决 SPA 应用的 SEO 问题

  6. Server-Side Rendering with React + React-Router



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

 
前端大全 更多文章 高效的 JavaScript 如何成为一名卓越的前端工程师 高效的 JavaScript 前端不为人知的一面——前端冷知识集锦 网页性能管理详解
猜您喜欢 第三只眼:熊猫乱谈——瓦查,IDE Java的常见误区与细节 最后1天 | 百度UI大咖分享:月薪3000还是年薪30万? DaoCloud 助力上海联通“沃+”大赛,探索数据变现之路 致等待跳槽的人——为什么我们不建议等到年后?