React Redux Sever Rendering实战

# React Redux Sever Rendering(Isomorphic JavaScript)

![React Redux Sever Rendering(Isomorphic)入门](http://obl1r1s1x.bkt.clouddn.com/isomorphic-javascript.png)

## 前言

由于可能有些读者没听过 [Isomorphic JavaScript](http://isomorphic.net/) 。因此在进到开发 React Redux Sever Rendering 应用程式的主题之前我们先来聊聊 Isomorphic JavaScript 这个议题。

根据 [Isomorphic JavaScript](http://isomorphic.net/) 这个网站的说明:

>Isomorphic JavaScript

Isomorphic JavaScript apps are JavaScript applications that can run both client-side and server-side.

The backend and frontend share the same code.

Isomorphic JavaScript 系指浏览器端和伺服器端共用 JavaScript 的程式码。

另外,除了 Isomorphic JavaScript 外,读者或许也有听过 Universal JavaScript 这个用词。那什麽是 Universal JavaScript 呢?它和 Isomorphic JavaScript 是指一样的意思吗?针对这个议题网路上有些开发者提出了自己的观点: [Universal JavaScript](https://medium.com/@mjackson/universal-javascript-4761051b7ae9#.67xsay73m)、[Isomorphism vs Universal JavaScript](https://medium.com/@ghengeveld/isomorphism-vs-universal-javascript-4b47fb481beb#.qvggcp3v8)。其中 Isomorphism vs Universal JavaScript 这篇文章的作者 Gert Hengeveld 指出 `Isomorphic JavaScript` 主要是指前后端共用 JavaScript 的开发方式,而 `Universal JavaScript` 是指 JavaScript 程式码可以在不同环境下运行,这当然包含浏览器端和伺服器端,甚至其他环境。也就是说 `Universal JavaScript` 在意义上可以涵盖的比 `Isomorphic JavaScript` 更广泛一些,然而在 Github 或是许多技术讨论上通常会把两者视为同一件事情,这部份也请读者留意。

## Isomorphic JavaScript 的好处

在开始真正撰写 Isomorphic JavaScript 前我们在进一步探讨使用 Isomorphic JavaScript 有哪些好处?在谈好处之前,我们先看看最早 Web 开发是如何处理页面渲染和 state 管理,还有遇到哪些挑战。

最早的时候我们谈论 Web 很单纯,都是由 Server 端进行模版的处理,你可以想成 template 是一个函数,我们传送资料进去,template 最后产生一张 HTML 给浏览器显示。例如:Node 使用的([EJS](http://ejs.co/)、[Jade](http://jade-lang.com/))、Python/Django 的 [Template](https://docs.djangoproject.com/el/1.10/ref/templates/) 或替代方案 [Jinja](https://github.com/pallets/jinja)、PHP 的 [Smarty](http://www.smarty.net/)、[Laravel](https://laravel.com/) 使用的 [Blade](https://laravel.com/docs/5.0/templates),甚至是 Ruby on Rails 用的 [ERB](http://guides.rubyonrails.org/layouts_and_rendering.html)。都是由后端去 render 所有资料和页面,前端处理相对单纯。

然而随著前端工程的软体工程化和使用者体验的要求,开始出现各式前端框架的百花齐放,例如:[Backbone.js](http://backbonejs.org/)、[Ember.js](http://emberjs.com/) 和 [Angular.js](https://angularjs.org/) 等前端 MVC (Model-View-Controller) 或 MVVM (Model-View-ViewModel) 框架,将页面于前端渲染的不刷页单页式应用程式(Single Page App)也因此开始流行。

后端除了提供初始的 HTML 外,还提供 API Server 让前端框架可以取得资料用于前端 template。複杂的逻辑由 ViewModel/Presenter 来处理,前端 template 只处理简单的是否显示或是元素迭代的状况,如下图所示:

![React Redux Sever Rendering(Isomorphic)入门](http://obl1r1s1x.bkt.clouddn.com/isomorphic-api.png)

然而前端渲染 template 虽然有它的好处但也遇到一些问题包括效能、SEO 等议题。此时我们就开始思考 Isomorphic JavaScript 的可能性:为什麽我们不能前后端都使用 JavaScript 甚至是 React?

![React Redux Sever Rendering(Isomorphic)入门](http://obl1r1s1x.bkt.clouddn.com/client-mvc.png)

事实上,React 的优势就在于它可以很优雅地实现 Server Side Rendering 达到 Isomorphic JavaScript 的效果。在 `react-dom/server` 中有两个方法 `renderToString` 和 `renderToStaticMarkup` 可以在 server 端渲染你的 components。其主要都是将 React Component 在 Server 端转成 DOM String,也可以将 props 往下传,然而事件处理会失效,要到 client-side 的 React 接收到后才会把它加上去(但要注意 server-side 和 client-side 的 checksum 要一致不然会出现错误),这样一来可以提高渲染速度和 SEO 效果。`renderToString` 和 `renderToStaticMarkup` 最大的差异在于 `renderToStaticMarkup` 会少加一些 React 内部使用的 DOM 属性,例如:`data-react-id`,因此可以节省一些资源。

使用 `renderToString` 进行 Server 端渲染:

```javascript

import ReactDOMServer from 'react-dom/server';

ReactDOMServer.renderToString(<HelloButton name="Mark" />);

```

渲染出来的效果:

```html

<button data-react>

Hello, Mark

</button>

```

总的来说使用 Isomorphic JavaScript 会有以下的好处:

1. 有助于 SEO

2. Rendering 速度较快,效能较佳

3. 放弃蹩脚的 Template 语法拥抱 Component 元件化思考,便于维护

4. 尽量前后端共用程式码节省开发时间

不过要注意的是如果有使用 Redux 在 Server Side Rendering 中,其流程相对複杂,不过大致流程如下:

由后端预先载入需要的 initialState,由于 Server 渲染必须全部都转成 string,所以先将 state 先 dehydration(脱水),等到 client 端再 rehydration(覆水),重建 store 往下传到前端的 React Component。

而要把资料从伺服器端传递到客户端,我们需要:

1. 把取得初始 state 当做参数并对每个请求建立一个全新的 Redux store 实体

2. 选择性地 dispatch 一些 action

3. 把 state 从 store 取出来

4. 把 state 一起传到客户端

接下来我们就开始动手实作一个简单的 React Server Side Rendering app

## 专案成果截图

![image](http://obl1r1s1x.bkt.clouddn.com/isomorphic-app.png)

## Server Rendering

获取数据可以调用 action,routes 在服务器端的处理参考 react-router server rendering,在服务器端用一个 match 方法将拿到的 request url 匹配到我们之前定义的 routes,解析成和客户端一致的 props 对象传递给组件。

./devServer.js

```

var express = require('express');

var webpack = require('webpack');

var config = require('./webpack.config.dev');

import React from 'react';

import { renderToString } from 'react-dom/server';

import { RouterContext, match } from 'react-router';

import { Provider } from 'react-redux';

import createRouter from './client/routes';

import configureStore from './client/store';

var app = express();

var compiler = webpack(config);

import comments from './client/data/comments';

import posts from './client/data/posts';

// create an object for the default data

const defaultState = {

posts,

comments

};

app.use(require('webpack-dev-middleware')(compiler, {

noInfo: true,

publicPath: config.output.publicPath

}));

app.use(require('webpack-hot-middleware')(compiler));

function renderFullPage(html, initialState) {

return `

<!doctype html>

<html>

<head>

<meta charset="UTF-8">

<title>isomorphic-redux-app</title>

<link rel="shortcut icon" type="image/png" href="http://obl1r1s1x.bkt.clouddn.com/bitbug_favicon.ico"/>

</head>

<body>

<div ></script>

</body>

</html>

`;

}

app.use((req, res) => {

const store = configureStore(defaultState);

const routes = createRouter();

const state = store.getState();

match({ routes, location: req.url }, (err, redirectLocation, renderProps) => {

if (err) {

res.status(500).end(`Internal Server Error ${err}`);

} else if (redirectLocation) {

res.redirect(redirectLocation.pathname + redirectLocation.search);

} else if (renderProps) {

const html = renderToString(

<Provider store={store}>

<RouterContext {...renderProps} />

</Provider>

);

res.end(renderFullPage(html, store.getState()));

} else {

res.status(404).end('Not found');

}

});

});

app.listen(7770, 'localhost', function(err) {

if (err) {

console.log(err);

return;

}

console.log('Listening at http://localhost:7770');

});

```

服务器端渲染部分可以直接通过共用客户端 store.dispatch(action) 来统一获取 Store 数据。另外注意 renderFullPage 生成的页面 HTML 在 React 组件 mount 的部分(<div >),前后端的 HTML 结构应该是一致的。然后要把 store 的状态树写入一个全局变量(__INITIAL_STATE__),这样客户端初始化 render 的时候能够校验服务器生成的 HTML 结构,并且同步到初始化状态,然后整个页面被客户端接管。

本项目地址:[React-Redux-Server-Rendering](https://github.com/cllgeek/React-Redux-Server-Rendering)