對服務端渲染的一次完全實踐

xiyuyizhi發表於2019-03-04

之前react技術棧做的一個應用,最近把首頁改成了服務端渲染的形式,過程還是很周折的,踩到了不少坑,記錄一些重點,希望有所幫助

前端使用的技術棧

  • react、react-dom 升級到 v16

  • react-router-dom v4

  • redux red-sage

  • antd-mobile 升級到 v2

  • ssr服務 express

專案地址, 喜歡的給個star,謝謝

訪問地址(手機模式)

非服務端渲染 服務端渲染

效果對比

nossr
ssr

前後處理流程對比

flow

react下ssr的實現方式

React下同構的解決方案有next.js、react-server等,這裡,因為這個專案之前已經採用create-react-app、redux做完了,只是想在現有系統基礎上把首頁改成服務端直出的方式,就選擇了webpack-isomorphic-tools這個模組

webpack-isomorphic-tools介紹

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

而webpack-isomorphic-tools就幫助我們處理了這些非js資源,在客戶端webpack構建過程中,webpack-isomorphic-tools作為一個外掛,生成了一份json檔案,形如:

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來處理302、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,感謝

相關文章