服务端渲染一个很常见的场景是当用户(或搜索引擎爬虫)第一次请求页面时,用它来做初始渲染。当服务器接收到请求后,它把需要的组件渲染成 HTML 字符串,然后把它返回给客户端(这里统指浏览器)。之后,客户端会接手渲染控制权。
下面我们使用 React 来做示例,对于支持服务端渲染的其它 view 框架,做法也是类似的。
服务端使用 Redux
当在服务器使用 Redux 渲染时,一定要在响应中包含应用的 state,这样客户端可以把它作为初始 state。这点至关重要,因为如果在生成 HTML 前预加载了数据,我们希望客户端也能访问这些数据。否则,客户端生成的 HTML 与服务器端返回的 HTML 就会不匹配,客户端还需要重新加载数据。
把数据发送到客户端,需要以下步骤:
- 为每次请求创建全新的 Redux store 实例;
- 按需 dispatch 一些 action;
- 从 store 中取出 state;
- 把 state 一同返回给客户端。
在客户端,使用服务器返回的 state 创建并初始化一个全新的 Redux store。
Redux 在服务端惟一要做的事情就是,提供应用所需的初始 state。
安装
下面来介绍如何配置服务端渲染。使用极简的 Counter 计数器应用 来做示例,介绍如何根据请求在服务端提前渲染 state。
安装依赖库
本例会使用 Express 来做小型的 web 服务器。还需要安装 Redux 对 React 的绑定库,Redux 默认并不包含。
npm install --save express react-redux
服务端开发
下面是服务端代码大概的样子。使用 app.use 挂载 Express middleware 处理所有请求。不熟悉 Express 或者 middleware,只需要了解每次服务器收到请求时都会调用 handleRender 函数。
另外,如果有使用 ES6 和 JSX 语法,需要使用 Babel (对应示例this example of a Node Server with Babel) 和 React preset。
server.js
import path from 'path'
import Express from 'express'
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import counterApp from './reducers'
import App from './containers/App'
const app = Express()
const port = 3000
// 提供静态文件
app.use('/static', Express.static('static'))
// 每当收到请求时都会触发
app.use(handleRender)
// 接下来会补充这部分代码
function handleRender(req, res) {
/* ... */
}
function renderFullPage(html, preloadedState) {
/* ... */
}
app.listen(port)
处理请求
第一件要做的事情就是对每个请求创建一个新的 Redux store 实例。这个 store 惟一作用是提供应用初始的 state。
渲染时,使用 <Provider> 来包住根组件 <App />,以此来让组件树中所有组件都能访问到 store,就像之前的搭配 React 教程讲的那样。
服务端渲染最关键的一步是在发送响应前渲染初始的 HTML。这就要使用 ReactDOMServer.renderToString()。
然后使用 store.getState() 从 store 得到初始 state。renderFullPage 函数会介绍接下来如何传递。
import { renderToString } from 'react-dom/server'
function handleRender(req, res) {
// 创建新的 Redux store 实例
const store = createStore(counterApp)
// 把组件渲染成字符串
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// 从 store 中获得初始 state
const preloadedState = store.getState()
// 把渲染后的页面内容发送给客户端
res.send(renderFullPage(html, preloadedState))
}
注入初始组件的 HTML 和 State
服务端最后一步就是把初始组件的 HTML 和初始 state 注入到客户端能够渲染的模板中。如何传递 state 呢,我们添加一个 <script> 标签来把 preloadedState 赋给 window.PRELOADED_STATE。
客户端可以通过 window.PRELOADED_STATE 获取 preloadedState。
同时使用 script 标签来引入打包后的 js bundle 文件。这是打包工具输出的客户端入口文件,以静态文件或者 URL 的方式实现服务端开发中的热加载。下面是代码。
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// 警告:关于在 HTML 中嵌入 JSON 的安全问题,请查看以下文档
// http://redux.js.org/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
/</g,
'\\u003c'
)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}
未完待续...