服務(wù)端渲染一個(gè)很常見(jiàn)的場(chǎng)景是當(dāng)用戶(或搜索引擎爬蟲(chóng))第一次請(qǐng)求頁(yè)面時(shí)费彼,用它來(lái)做初始渲染箍邮。當(dāng)服務(wù)器接收到請(qǐng)求后坝锰,它把需要的組件渲染成 HTML 字符串处窥,然后把它返回給客戶端(這里統(tǒng)指瀏覽器)雹仿。之后增热,客戶端會(huì)接手渲染控制權(quán)。
下面我們使用 React 來(lái)做示例胧辽,對(duì)于支持服務(wù)端渲染的其它 view 框架峻仇,做法也是類似的。
服務(wù)端使用 Redux
當(dāng)在服務(wù)器使用 Redux 渲染時(shí)邑商,一定要在響應(yīng)中包含應(yīng)用的 state摄咆,這樣客戶端可以把它作為初始 state。這點(diǎn)至關(guān)重要人断,因?yàn)槿绻谏?HTML 前預(yù)加載了數(shù)據(jù)豆同,我們希望客戶端也能訪問(wèn)這些數(shù)據(jù)。否則含鳞,客戶端生成的 HTML 與服務(wù)器端返回的 HTML 就會(huì)不匹配影锈,客戶端還需要重新加載數(shù)據(jù)。
把數(shù)據(jù)發(fā)送到客戶端蝉绷,需要以下步驟:
- 為每次請(qǐng)求創(chuàng)建全新的 Redux store 實(shí)例鸭廷;
- 按需 dispatch 一些 action;
- 從 store 中取出 state熔吗;
- 把 state 一同返回給客戶端辆床。
在客戶端,使用服務(wù)器返回的 state 創(chuàng)建并初始化一個(gè)全新的 Redux store桅狠。
Redux 在服務(wù)端惟一要做的事情就是讼载,提供應(yīng)用所需的初始 state轿秧。
安裝
下面來(lái)介紹如何配置服務(wù)端渲染。使用極簡(jiǎn)的 Counter 計(jì)數(shù)器應(yīng)用 來(lái)做示例咨堤,介紹如何根據(jù)請(qǐng)求在服務(wù)端提前渲染 state菇篡。
安裝依賴庫(kù)
本例會(huì)使用 Express 來(lái)做小型的 web 服務(wù)器。還需要安裝 Redux 對(duì) React 的綁定庫(kù)一喘,Redux 默認(rèn)并不包含驱还。
npm install --save express react-redux
服務(wù)端開(kāi)發(fā)
下面是服務(wù)端代碼大概的樣子。使用 app.use 掛載 Express middleware 處理所有請(qǐng)求凸克。不熟悉 Express 或者 middleware议蟆,只需要了解每次服務(wù)器收到請(qǐng)求時(shí)都會(huì)調(diào)用 handleRender 函數(shù)。
另外萎战,如果有使用 ES6 和 JSX 語(yǔ)法咐容,需要使用 Babel (對(duì)應(yīng)示例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
// 提供靜態(tài)文件
app.use('/static', Express.static('static'))
// 每當(dāng)收到請(qǐng)求時(shí)都會(huì)觸發(fā)
app.use(handleRender)
// 接下來(lái)會(huì)補(bǔ)充這部分代碼
function handleRender(req, res) {
/* ... */
}
function renderFullPage(html, preloadedState) {
/* ... */
}
app.listen(port)
處理請(qǐng)求
第一件要做的事情就是對(duì)每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的 Redux store 實(shí)例蚂维。這個(gè) store 惟一作用是提供應(yīng)用初始的 state疟丙。
渲染時(shí),使用 <Provider> 來(lái)包住根組件 <App />鸟雏,以此來(lái)讓組件樹(shù)中所有組件都能訪問(wèn)到 store享郊,就像之前的搭配 React 教程講的那樣。
服務(wù)端渲染最關(guān)鍵的一步是在發(fā)送響應(yīng)前渲染初始的 HTML孝鹊。這就要使用 ReactDOMServer.renderToString()炊琉。
然后使用 store.getState() 從 store 得到初始 state。renderFullPage 函數(shù)會(huì)介紹接下來(lái)如何傳遞又活。
import { renderToString } from 'react-dom/server'
function handleRender(req, res) {
// 創(chuàng)建新的 Redux store 實(shí)例
const store = createStore(counterApp)
// 把組件渲染成字符串
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// 從 store 中獲得初始 state
const preloadedState = store.getState()
// 把渲染后的頁(yè)面內(nèi)容發(fā)送給客戶端
res.send(renderFullPage(html, preloadedState))
}
注入初始組件的 HTML 和 State
服務(wù)端最后一步就是把初始組件的 HTML 和初始 state 注入到客戶端能夠渲染的模板中苔咪。如何傳遞 state 呢,我們添加一個(gè) <script> 標(biāo)簽來(lái)把 preloadedState 賦給 window.PRELOADED_STATE柳骄。
客戶端可以通過(guò) window.PRELOADED_STATE 獲取 preloadedState团赏。
同時(shí)使用 script 標(biāo)簽來(lái)引入打包后的 js bundle 文件。這是打包工具輸出的客戶端入口文件耐薯,以靜態(tài)文件或者 URL 的方式實(shí)現(xiàn)服務(wù)端開(kāi)發(fā)中的熱加載舔清。下面是代碼。
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// 警告:關(guān)于在 HTML 中嵌入 JSON 的安全問(wèn)題曲初,請(qǐng)查看以下文檔
// 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>
`
}
未完待續(xù)...