React 服务端渲染

 2016年03月01日    3066     声明


React 组件会在虚拟DOM中完成渲染,并通过虚拟DOM来更新DOM在浏览器中的变化。React 对组件的渲染是由 JavaScript 的完成的,也就是说:只有 JavaScript 在浏览器加载完成后才会开始组件的渲染。这种DOM管理机制会存在一些问题,如:搜索引擎抓取不到站点内容、对站点性能造成一定的影响。React 虚拟DOM是DOM在内存中的表现形式,这为React 在浏览器环境中运行提供了可能。React 可以从虚拟DOM中生成一个字符串(而非更新真正的DOM),这使我们可以从客户端和服务端使用同一个组件。


  1. 渲染方法
  2. 服务端渲染与状态共享

1. 渲染方法

在服务器端渲染React 组件时,无法使用React.render()方法,因为服务端没有DOM。React 提供了两个用于服务端渲染组件的方法:ReactDOMServer.renderToString()ReactDOMServer.renderToStaticMarkup()。这两个方法都会将虚拟DOM生成一个HTML字符串,这两个方法移除了服务器端对于浏览器环境的依赖,让React 组件在服务器端渲染成为可能。

这个方法都由React的ReactDOMServer对象提供。

1.1 renderToString()

ReactDOMServer.renderToString()是开发中主要使用的一个方法。该方法不同于ReactDOM.render(),它只接受一个参数,没有render()方法中表示渲染位置的参数。renderToString()是一个同步(阻塞式)方法,渲染速度非常快,渲染完成后会返回渲染后的字符串。

var MyComponent = React.createClass({
  render: function() {
    return (<h2>itbilu.com</h2>);
  }
});

var html = ReactDOMServer.renderToString(<MyComponent />);

上面示例渲染完成后,返回如下一个字符串:

<h2 data-reactid=".0" data-react-checksum="-472380432">itbilu.com</h2>

React 渲染组件后,为生成的HTML元素增加了两个data-前缀的特性。其中data-reactid被React 用于区分DOM节点,当propsstate发生变化时,React 可以根据此特性快速的更新DOM。data-react-checksum是对创建DOM的校验值,这使得React 可以客户端复用与服务端结构相同的代码,这一特性只会被添加到根元素上。


1.2 renderToStaticMarkup()

ReactDOMServer.renderToStaticMarkup()方法同样是用于在服务端渲染React 组件,该方法除渲染的HTML不会包含data0-开头的特性,除此之外与renderToStaticMarkup()没有任何区别。

var MyComponent = React.createClass({
  render: function() {
    return (<h2>itbilu.com</h2>);
  }
});

var html = ReactDOMServer.renderToStaticMarkup(<MyComponent />);

以上示例返回HTML如下:

<h2>itbilu.com</h2>


1.3 渲染方法的选择

renderToString()renderToStaticMarkup()方法功能非常相似,我们应该根据自己的需要选择所要使用的方法。

renderToStaticMarkup()方法仅当所渲染的组件不打算在客户端使用时才应该选择使用。

大多数我们应该选项renderToString(),React 会使用data-react-checksum快速的在客户端初始化一个组件,React 会直接使用服务端提供的DOM,这样就省略了创建DOM节点及将组件挂载到DOM中的过程。减少了加载时间,而用户可以更快的与站点交互。

React 会根据data-react-checksum检查客户端和服务端渲染的页面结构是否一致。当检测data-react-checksum值不一样时,React 舍弃服务端提供的DOM,然后重新渲染组件并将其挂载到页面中,这种情况下将不再拥有服务端渲染带来的性能优势。


2. 服务端渲染与状态共享

在设计服务端渲染组件时,应考虑如何将state传递给客户,以便充分利用服务端渲染的优势。同时要保证将同一个props传递到组中,总会输出相同的初始渲染结果,这样做可以保证组件客户端与服务端的一致性,并且提升了组件的可测试性。

2.1 checksum的失效

如,对于如下一个在服务端渲染的组件来说,其每次都会输出一个随机数:

var MyComponent = React.createClass({
  render: function() {
    return (<h2>{Math.random()}</h2>);
  }
});

var html = ReactDOMServer.renderToString(<MyComponent />);

这个组件会导致data-react-checksum值检测失败,React 会抛弃服务端的渲染结果,而在客户端重新进行渲染,这样就失去了服务端渲染的优势。


2.2 问题解决

要解决这个问题,需要对组件进行重构。我们可以将随机数封装到props中,并将props传递到客户端共享状态。

重构组件

我们将上面的组件重构如下(MyComponent.js),重构后MyComponent组件即可以在服务端使用,又可以客户端使用且客户端可以使用服务端的状态:

var MyComponent = React.createClass({
  render: function() {
    return (<h2>{this.props.number}</h2>);
  }
});

服务端渲染

服务端渲染组件后,除了渲后的HTML传递到页面外,还要将客户端使用的props传递到页面(index.js):

var num = Math.random();
// 服务端渲染HTML
var html = ReacDOMServer.renderToString(<MyComponent number={num} />);
// 向页面传递渲染后HTML字符串和num(props)
res.render('random-props', {html:html, num:num});

客户端渲染

客户端会在页面加载完成,使用服务端传递的props重新渲染组件,由于前后端的props值一样,React 的checksum检测也会通过(main.js):

// 客户端引入MyComponent组件
var MyComponent = require('./MyComponent');

var mountNode = document.getElementById('example');
// 使用服务端传递初始值重新渲染组件
ReactDOM.render(<MyComponent number={initProps.num} />, mountNode);

渲染后的页面

由于使用了服务端渲染,页面加载前组件已经挂载到了页面上。为了使用服务端的状态,props值也同样被渲染到了页面。将状态值渲染到页面是最简单的一种状态共享方式,还有其它一些状态共享的方法,我们将在后面的文章中介绍。(渲染前的页面:random-props.ejs

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>React 服务端渲染</title>
  </head>
  <body>
    <!-- 挂载React组件 -->
    <div id="example"><h2 data-reactid=".2fo4xn5iccg" data-react-checksum="-196341818">0.008809832856059074</h2></div>
    <script type="text/javascript">
      // 传递服务端状态
      var initProps = {num:0.008809832856059074};
    </script>
    <!-- main.js包含通过gulp和browserify 打包的React、MyComponent组件等 -->
    <script src="/main.js"></script>
  </body>
</html>


注意:页面中使用的main.js文件并不是前面客户渲染时使用的文件,而通过gulpbrowserify 打包后的文件,其中包括了客户端组件操作代码、React 组件及React相关依赖等。所使用的gulp脚本为:Gulpfile.js。示例完整代码:server-render

本文示例服务端基于Node.js的Express框架,后续会有ReactExpress框架集成使用的详细教程推出,请关注。