React SSR 教學與問題整理 淺談伺服器渲染觀念與常見問題

Lin Yen-Cheng on 2017-08-31 10 min. read

簡介

隨著瀏覽器的效能開始越來越好,帶有顯示邏輯的渲染可以轉移到瀏覽器端,於是也就慢慢出現了 Single Page Application 的概念,和以往散落在好幾頁的做法不同,現在將所有網頁程式都 bundle 成一個檔案,可以把 bundle 想像成是 APP 的 apk 檔,畫面中的元素不再透過後端直接產生在頁面中,而是透過前端產生,前端像手機 APP 一樣透過 API 跟後端溝通,透過要回來的新資料搭配顯示邏輯來渲染,舉個簡單的例子來說,登入之後不需要跳轉至新的頁面並重新載入,只需要透過要回來的資料來更改網頁中的元素(元件)即可~

那為什麼還會需要 SPA 的伺服器端渲染呢? 因為全部網頁都 bundle 起來載入速度慢,頁面中的元素會需要等到 bundle 都下載下來並去後端取回資料後才會呈現,雖然可以透過 webpack code splitting 單元中介紹的,去把自己的程式碼和用到的函式庫分離開來,但仍然沒有傳統後端直接產生來的快速,所以最後理想的狀況就是,第一頁由伺服器端渲染,並在伺服器時就把一些基礎的資料存在狀態中(像是熱門購物推薦清單),之後就都是 client 端的事情,Client 也不需要在重複向後端取得新的熱門購物清單,好處在第一頁的載入和傳統方式相同,之後的優點就是不需要再一直瘋狂向伺服器要整頁的元素了,所以 client 端在操作上也不會有整個頁面重新刷新的問題,這樣兩種的優點都能夠兼得,加上還不是全部的搜尋引擎都能夠爬取 SPA 中的內容,所以 SPA 的伺服器端渲染還是有存在必要!

實作小關鍵

在實作上會遇到的問題:

  1. 由於 SPA 是由狀態來決定元件的顯示,那伺服器端的狀態該如何決定呢? 後端渲染出來的會不會和前端渲染出來的有差異? (RWD?)
  2. 如果有使用狀態管理的函式庫像是 Redux 需要特別處理嗎?
  3. 前端 SPA 用的路由會影響嗎? 畢竟伺服器端並沒有 SPA 的路由

渲染時 react 會幫我們確認 markup 的 checksum 是否相同,若不同就會在 client 端重新渲染一次,若是如此,便失去了做 SSR 的意義,解決上後端 redux 的 SSR 可以利用初始或運算後的狀態去渲染頁面,但之後需要將這個狀態也傳回前端,前端接收到之後再重新產生 store 傳入 APP 中。

另外前端如果有用到像是 window.innerWidth 這種操作去做條件渲染時就一定會有問題,不正確的狀態或是沒考慮過的操作都會 markup 的 checksum 有錯誤,要特別謹記,若有用到 router 的部分不管是前後端都要使用 match 來避免前後端不一致~

Server Side Rendering Issues

  1. React-Router 3?
    要利用 match 然後使用 RouterContext,還有一些情境可能會用到 StaticRouter

  2. Redux?
    先用初始狀態去做出一個 Store,如果要加上從後端計算後的資料,那就去 dispatch 相關 action 然後從取得資料後的新 Store 中重新取得狀態,最後在 renderToString 時把元件轉成 html 的字串,把先前取得的狀態整合進去,就是已經帶有相關資料的狀態及 render 後的元件了

  3. React-Helmet?
    按照文件就是要多寫一行const helmet = Helmet.renderStatic();來避免記憶體洩漏的問題,做快取可能?會有預料之外的狀況,同時若有使用樣版引擎,meta data 中的動態資料整合方式會類似步驟 2

  4. style-component?
    也是需要按照文件去注入相關資訊

  5. 渲染時有用到 innerWidth or navigator 等瀏覽器才有的物件?
    悲劇? 盡量少用啦,因為 Server 沒有這些東西 Orz 真需要用也必須等到元件 mount 上來以後,這就意味著不可以利用這些物件來做條件渲染

  6. Server-side bundle?
    較簡易的實作方式是用 Node.js 語法來撰寫 Server 端,從以上情況看來,Client 端用到的套件幾乎在 Server 端都會再用到,那如果要減少 Server-side bundle 大小來爭取執行速度時,也就是 nodemodule 不一起 build 進來的情況下,webpack 的 external 設定要設一下,上傳到伺服器時,會需要重新 npm install 因為你有用到 Orz 其他要注意的大概是 webpack target: nodelibraryTarget: commonjs2

  7. isomorphic-fetch 在 node 端會需要絕對路徑

  8. 前端路由與後端路由衝突,在頁面導向前,如果是 React Create Script 的專案,前端可以使用 serviceWorker.unregister();

解決方案

  • 建議使用 Next.js,大部分問題有幫我們解決掉,人力不足時,推薦使用。
  • prerender-spa-plugin 這類外掛用於使用 Create Script 時的方案,但很可能會跟不上 React 的升級速度而產生問題,像是 lazy load 的部分。

Next.js

如果沒有太多奇怪的需求,開發初期覺得可以直接使用 Next.js 已經把大部分的問題進行解決,如果有特殊需求官方也都有很好的範例,缺點就是需要自己去組合出自己想要的專案架構,但我想大家都很擅長當複製貼上攻城獅,這樣的學習抄襲曲線應該還算可以接受,當然思考專案的方式可能也會改變成 Next 的方法,不過核心概念還是一樣的~


喜歡這篇文章,請幫忙拍拍手喔 🤣

share