3 min read

React.js 服务端渲染

是否多此一举?

毕竟,从服务端渲染走到客户端渲染这一步,前端界可花了不少时间。

如果你对服务端渲染的必要性心存疑虑,不妨先看看这一篇文章。

但我还是在这儿简单介绍下,为什么这些客户端渲染的框架们要踏入服务端渲染的领域。

在客户端渲染时,我们的页面通常很简洁,比如,你可能见过这样的 HTML 文件:

<!doctype html>
<html>
  <head>
    <title>这是陈三</title>
  </head>
  <body>
    <script src='http://code.jquery.com/jquery-2.1.4.min.js'></script>
    <script src='http://example.com/test.bundle.js'></script>
  </body>
</html>

陈三在打开这个页面后,浏览器会加载:

  1. jquery-2.1.4.min.js
  2. test.bundle.js

在这两个文件加载完成并执行以前,陈三只能看到一片空白。等了十来秒后,页面还没有动静,陈三找来专业人士,哦,jquery-2.1.4.min.js 文件不知道怎么回事,加载失败,导致 test.bundle.js 无法构建 HTML 代码。

实际上:

  1. 任何 js 代码问题都可能导致陈三看不到页面
  2. 搜索引擎可能无法索引这样的页面(对,Google 可以做到,但不是所有搜索引擎都是 Google)

那么,我们可以做得更好吗?

这正是 Ember.js、Angular.js 等努力的方向。这一篇,则是介绍 react.js 在这方面的情况。

renderToString

react 官网文档在服务端渲染上着墨不多

Render a ReactElement to its initial HTML. This should only be used on the server. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

If you call ReactDOM.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

简单说,renderToString 方法在服务器上先渲染出组件的 HTML 结构,客户端上 react 在执行到 render() 方法时,会检查是否有服务端渲染过的代码,如果有,则仅仅往上添加事件处理器。我们不妨这样认为,原来在客户端要执行的 js 代码被拆成两个部分:

  1. 渲染 HTML 结构
  2. 往 HTML 结构上附加事件处理器

前者在服务端上完成,后者在客户端上完成。

那么,我们要怎么入手 react.js 服务端渲染?react.js 初期开发者 Pete Hunt 给了些建议

Server rendering still requires a lot of tooling to get right. Since it transparently supports React components written without server rendering in mind, you should build your app first and worry about server rendering later. You won’t need to rewrite all of your components to support it.

要做服务端渲染,我们要干的事还很多,但通常可以先构建 app,再来关心服务端渲染。我们并不会需要全部重写所有的组件。

准备工作

我们要先解决以下问题:

  1. server 端用什么框架?

    对前端开发来说,基于 node.js 的框架通常更易于上手,所以这里选用了 expressjs

  2. 要构建出前端使用的 js 文件,要用什么工具?

    不论是 requirejs 还是 browserify 或 jspm 都可以用,这里使用 webpack。

  3. react.js 在后端 render 时,如果要用 jsx 语法,要怎么解决?

    使用 babel-register

  4. 如果我想用 es2015 语法后 express.js 程序,要怎么做?

    同第 3 点。

接下来就是安装 react、react-dom、babel,并且配置 babel,点击查看项目代码

好了,我们可以开始写代码了。

按部就班

首先,我们添加一个 Home 组件,代码如下:

import React from 'react'
export default class Home extends React.Component {
  render () {
    return (<div>
              hello from 陈三。
            </div>)
  }
}

这个组件目前只是渲染一段 div。

接下来是在 app.js 文件中引用它并渲染成 HTML:

import express from 'express'
import React from 'react'
import ReactDOM from 'react-dom/server'
import Home from './components/Home'
let app = express()
app.get('/', (req, res) => {
  res.send(ReactDOM.renderToString(React.createFactory(Home)()))
})
app.listen(3000, () => {
  console.log('listen on 3000')
})

这里用到的两个方法:

  1. renderToString – 将组件渲染成字符串。
  2. createFactory – 生成一个组件工厂方法,用于生成组件。之所以用它,是因为 renderToString 接受的参数是 ReactElement element,所以我们不能使用 jsx 的形式 <Home />

执行 node index.js 命令,可以在 http://localhost:3000 网址看到:

hello from 陈三。

点击查看这一步的项目代码

目前为止,我们还没给组件添加任何交互行为,比如点击一下,字体颜色变化。下面我们就来尝试给 Home 组件添加这一交互。

我们将 Home 组件改造如下:

import React from 'react'
export default class Home extends React.Component {
  constructor (props) {
    super(props)
    this.clickToChangeColor = this.clickToChangeColor.bind(this)
    this.state = {
      color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
    }
  }
  clickToChangeColor (e) {
    this.setState({
      color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
    })
  }
  render () {
    return (<div onClick={this.clickToChangeColor} style={{color: this.state.color}}>
              hello from 陈三。
            </div>)
  }
}

现在点击 div 块,字体颜色并不会出现任何变化,因为,目前为止,我们只是在后端渲染了 HTML 结构,事件绑定等工作,要在前端上完成。

但现在,我们还只是简单的生成一个页面,只有一个 div 块,没有 html 标签,没有 meta 标签,也没有引用任何脚本。

我们需要一个完整的页面,这里,使用 jade 模板引擎来生成页面。

我们在 views 目录下增加一个 index.jade 模板:

doctype html
html
    head
        title='react 服务端渲染'
        meta(charset='utf-8')
    body
        #root!= react
        script(src='/bundle.js')

app.js 内容修改成:

import express from 'express'
import React from 'react'
import ReactDOM from 'react-dom/server'
import Home from './components/Home'
let app = express()
app.use(express.static('public'))
app.set('views', './views')
app.set('view engine', 'jade')
let html = ReactDOM.renderToString(React.createFactory(Home)())
app.get('/', (req, res) => {
  res.render('index', {react: html})
})
app.listen(3000, () => {
  console.log('listen on 3000')
})

目前为止,我们还没有一个叫 bundle.js 的文件,它将由 webpack 来构建。

构建 bundle.js

如果对 webpack 的构建不熟悉,可以先看看 我写的 webpack 教程。这里跳过安装 webpack、babel 等步骤。

上面说到的 app.js 是服务端的入口文件,前端上同样需要一个入口,且把它叫 client.js:

import Home from './components/Home'
import ReactDOM from 'react-dom'
import React from 'react'
ReactDOM.render(<Home />, document.getElementById('root'))

另外,我们再定义一个 webpack.config.js 文件:

var path = require('path')
module.exports = {
  entry: './src/client',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'public')
  },
  module: {
    loaders: [
      {test: /\.js$/,
        loaders: ['babel?presets[]=react,presets[]=es2015'],
        include: [path.join(__dirname, 'src')]
      }
    ]
  }
}

在项目根目录下执行 webpack,我们就会得到 public/bundle.js,刷新我们的主页,点击 div 就能看到文字的颜色在改变了。

就这样,我们完成了一趟轻松、简单的 react.js 服务端渲染之旅。

如果对完整的代码有兴趣,请点击 github 上的 react-server-render 仓库,还可以点击示例查看最终效果。

报告问题 修订

如果你有自建 https 代理的需求,欢迎尝试 Phantom,一键搭建,方便快捷。查看 demo