對服務端渲染的一次徹底實踐

以前react技術棧作的一個應用,最近把首頁改爲了服務端渲染的形式,過程仍是很周折的,踩到了很多坑,記錄一些重點,但願有所幫助css

前端使用的技術棧

  • react、react-dom 升級到 v16html

  • react-router-dom v4前端

  • redux red-sagenode

  • antd-mobile 升級到 v2react

  • ssr服務 expresswebpack

項目地址, 喜歡的給個star,謝謝nginx

訪問地址(手機模式)

非服務端渲染 服務端渲染

效果對比

nossr
ssr

先後處理流程對比

flow

react下ssr的實現方式

React下同構的解決方案有next.js、react-server等,這裏,由於這個項目以前已經採用create-react-app、redux作完了,只是想在現有系統基礎上把首頁改爲服務端直出的方式,就選擇了webpack-isomorphic-tools這個模塊git

webpack-isomorphic-tools介紹

若是咱們想在現有React系統中引入同構,首先要解決的一個重要問題是:代碼中咱們import了圖片,svg,css等非js資源,在客戶端webpack的各類loader幫咱們處理了這些資源,在node環境中單純的依靠babel-regisiter是不行的,執行renderToString()會報錯,非js資源無法處理github

而webpack-isomorphic-tools就幫助咱們處理了這些非js資源,在客戶端webpack構建過程當中,webpack-isomorphic-tools做爲一個插件,生成了一份json文件,形如:web

isomorphic-json

有了這份映射文件,在同構的服務端,renderToString()執行的過程當中,就能夠正確的處理那些非js資源

好比咱們有一個組件:

const App =()=>{
    return <img src={require('../common/img/1.png')}>
}

同構的服務端調用renderToString(<App />),就生成正確的

<img src="static/media/1.3b00ac49.png">標籤

複製代碼

對webpack-isomorphic-tools的具體使用參見github

實現ssr須要解決的問題

  1. 非js資源引用的處理,上面已經說過

  2. 初始redux store數據的獲取(即保證請求的服務端渲染的頁面和單純請求的首頁的狀態一致)

  3. 路由跳轉如何處理

  4. 用戶在客戶端登陸了,從新請求服務端頁面,服務端如何加入用戶已登陸了的新狀態

  5. 用戶訪問了服務端渲染的首頁,客戶端js加載完後仍是會執行,組件componentDidMount()中的ajax請求如何避免觸發

額,一一個說

初始redux store數據的獲取

簡單總結就是

  1. 咱們請求了ssr服務,服務在給咱們吐頁面以前,實例化一個createStore()對象,要將本來在客戶端初始請求的那幾個ajax在這發,這幾個請求完成後都dispatch(action),而後store中就有初始狀態了

  2. 而後執行

renderToString(<Provider store={store}>
        <Router location={req.baseUrl}
            context={context}>
            <Routes />
        </Router>
    </Provider>)
 //獲得填滿數據的標籤  

複製代碼
  1. 拼接html

注意,上面說的webpack-isomorphic-tools中生成的json文件中有js,css的對應關係,這裏我訪問那個json文件獲得js、css的路徑,拼到html中

還要返回store中保存的狀態,供客戶端js createStore使用

<script>
        window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}
    </script>
複製代碼
  1. 在客戶端js中
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
    reducer,
    window.__INITIAL_STATE__,
    applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

複製代碼

路由

在作同構的時候不能用BrowserRouter,要使用無狀態的StaticRouter,並結合location和context兩個屬性

有這樣的路由結構

<div className="main">
    <Route exact path="/" render={() =>
        <Redirect to="/home"></Redirect>
    }></Route>
    <Route path="/home" component={Home}></Route>
    <Route path="/detail/:id" component={Detail}></Route>
    <Route path="/user" component={User}></Route>
    <Route path="/reptile" component={Reptile}></Route>
    <Route path="/collect" component={Collect}></Route>
</div>
//默認跳到/home,其餘的該到哪到哪
複製代碼

server端的代碼要這樣

const context = {}
const html = renderToString(
    <Provider store={store}>
        <Router location={req.baseUrl}
            context={context}>
            <Routes />
        </Router>
    </Provider>)
//<Route>中訪問/,重定向到/home路由時
if (context.url) {
    res.redirect('/home')
    return
}
複製代碼

StaticRouter能夠根據request來的url來指定渲染哪一個組件,context.url指定重定向到的那個路由

也就是說,要是訪問 /,StaticRouter會給咱們重定向到/home,而且StaticRouter自動給context對象加了url,context.url就是重定向的/home,當不是重定向時,context.url是undefined

咱們還能夠本身寫邏輯 經過context來處理30二、404等。但這裏我不須要。。。。。,爲何呢?

我沒作全棧的同構,只服務端渲染了主頁,渲染一個和多個差很少,全都渲染的話就是在服務端要根據當前請求的路由來決定要發那些請求來填充Store

我對路由的處理流程上面的思惟導圖有說明,就是在nginx中多配一個代理。

對於訪問/、/home這兩個路由,代理到ssr服務,來吐首頁內容,api代理到後端服務,其餘的直接返回(也就是說若是在detail頁面或user頁面刷新了頁面仍是以前客戶端渲染那套)

對登陸操做的處理

上面說server端初始化數據的時候還有一個登錄問題沒說。

用戶初始訪問了服務端渲染的首頁,而後在客戶端轉到登陸頁面登錄了,從新回到首頁刷新了頁面,喔,又去請求了ssr服務,但服務端不知道當前用戶登陸了啊,仍是原來的流程,返回的__INITIAL_STATE__中仍是沒有用戶的我的信息和已登陸狀態

因此,在客戶端登錄後,要將用戶的token存到cookie中,這樣,在首頁就算用戶刷新了頁面,從新請求頁面請求中也會帶上cookie,在服務端,根據request.cookies中是否有token來決定發哪些請求填充store

if (auth) {
    //要是有token就去查用戶信息和是否登陸狀態(還查是否登陸是由於token有多是被篡改過的)
        promises = [
            getMoviesList(store, auth),
            getCategory(store),
            checkLogin(store, auth),
            getUinfo(store, auth)
        ]
    } else {
        promises = [
            getMoviesList(store),
            getCategory(store),
        ]
}
Promise.all(promises).then(x=>{
    renderToString(<Provider store={store}></Provider>)
})

複製代碼

避免客戶端js中初始請求的觸發

到這一步,訪問域名,就可以正確展現服務端渲染的頁面,跳到別的路由,客戶端的js也能正常處理接下來的事,可是,服務端渲染頁面展現後,首頁那幾個ajax請求仍是觸發了,這是不必的。

原覺得這是react renderToString()生成的標籤和客戶端js hydrate()的有差別致使的,然而,實際上,js執行了,組件的生命週期該觸發仍是會觸發的,不僅是attach event listeners to the existing markup

因此要手動避免

在App組件中

componentDidMount() {
        if (!window.__INITIAL_STATE__) {
            this.props.checkLogin()
            this.props.loadCategory()
        }
    }

//噹噹前頁面是服務端返回的(由於window.__INITIAL_STATE__有初始狀態),初始的ajax就不觸發了

複製代碼

總結

服務端渲染的坑仍是挺多的,這一個星期就搞它了。。。。這裏記錄一些比較重要的東西,具體細節有興趣的能夠看下代碼.最後,最重要的,喜歡的給個star,感謝

相關文章
相關標籤/搜索