前一篇文章講到了為了預取數據,各個組件的寫法。這里從整體上講一個client和server分別應該怎么做。
Server Side Rendering的一個明確目標其實就是等“異步”操作都結束了,再renderToString然后返回給客戶端。這樣,客戶端沒有javascript的情況下,依然可以看到數據(所以對爬蟲是友好的)。
我用到的庫是 react-redux, react-router, redux-saga,所以是要讓redux-saga能夠處理完必要的請求之后,進行第二次渲染,然后返回給客戶端。(用redux-thunk是一樣的道理, 需要等promise結束之后,再調用renderToString,然后返回給客戶端)
廢話不多說,下面是樣例代碼:
// express 處理請求入口
app.get('*', (req, res) => {
handleRender(req, res)
})
// 根據你自己的需求創建store, 主要參考redux就行了
function createStoreForServer() {
const sagaMiddleware = createSagaMiddleware()
middlewares = [sagaMiddleware] // 根據需求自己可以加入其它中間件
let preloadedState = {} // 客戶端需要在這個地方加載服務器端傳過來的初始狀態, 詳見redux文檔(http://redux.js.org/docs/recipes/ServerRendering.html)
let store = createStore(rootReducer, {}, applyMiddleware(...middlewares))
// 下面是關鍵點, 這些方法是server端需要用到的
store.runSaga = () => sagaMiddleware.run(rootSaga)
store.close = () => store.dispatch(END)
return store
}
function handleRender(req, res) {
let store = createStoreForServer()
// 判斷saga的調用都結束了, 然后開始第二次渲染
store.runSaga().done.then(() => {
const html = renderToString(<Routes store={store} />)
res.send(renderFullPage(html, store.getState())) // renderFullPage 參見redux文件就行了
}
// 觸發第一次渲染, 可是返回值我們并不關心, 只要改變store即可
renderToString(<Routes store={store} />)
// 關停saga, 第二次渲染的時候,忽略各種請求就好啦
store.close()
}
上面這些一做,基本上就搞定啦。服務器端渲染,只需引入了這么一小段代碼,就可以解決核心問題了。
我這里沒有提到的問題還有(每個小點感覺都可以專門寫一篇博客了):
- 取用戶私有數據怎么辦? 靠cookie。如何做呢?我自己的實現并不完美,是把一些信息暫時放在store中了,但是
react-cookie
可能有更好的解決辦法(我一時半會沒搞清楚怎么跟redux結合,就沒啟用)。 - 官方文檔中,renderFullPage中需要把html的結構直接寫在函數里面,可是html的內容可能是動態生成的,怎么辦?首先,為啥會動態生成呢, 因為生產環境打包的時候,js, css是需要帶hash的,因此html引用的js, css的名字會變化。動態生成我用了
HtmlWebpackPlugin
。其次, 有個html文件作為模板了,怎么用renderFullPage
? 我用的是個超簡單又笨的方法:讀取html文件,然后字符串替換。 - react-router在server,client需要用到不用的類型的history,怎么處理?這個在react-router的github上搜一搜就解決方案。 我用的是
BrowserRouter
和StaticRouter
,注意StaticRouter
可能有重定向信息就是了。
最后的最后, 有人給我推薦過next.js
,據說可以方便解決SSR問題,我大概看了一下, 跟原生的寫法還是有一些不同的,如果新項目,可以考慮在開始的時候就啟用。這次我踩的坑已經夠多,就沒有去用next.js
了(也是有點小擔心react 16.0
可能跟next.js
會不兼容,導致我到時候不能順暢升級)。