React | 用實作了解 Server-Side Rendering 的運作原理-Redux 篇

神Q超人
Starbugs Weekly 星巴哥技術專欄
7 min readAug 17, 2021
Photo by AbsolutVision on Unsplash

Hi!大家好,我是神 Q 超人!如果覺得今天的標題有種既視感,那請別懷疑自己,因為這是一篇被我遺忘很久的文章後續 😂。記得去年寫了「React | 用實作了解 Server-Side Rendering 的運作原理」,那時候就有規劃要繼續寫關於導入 Redux 和 call API 的部分,但後來不知為何的被我遺忘了 😅。

不過沒關係!雖然大概過了一年半,但該補的坑還是來補。在這篇文章裡不會提到太多有關 SSR 的觀念,因為上一篇文章就已經有提過了,不過在開始導入 Redux 和 call API 之前,還是會稍微複習之前做了什麼,然後再接下去繼續說明 🙌!

複習時間

在上一篇中,我們為了沒有 Redux 也沒有 call API 的網頁做了什麼來導入 SSR 呢?其實只有三件事情而已:

建立 Server

我們用 NodeJS 的 express 套件建立 server,主要的目的是用來接收當前瀏覽器的請求,以及回傳要顯示的第一個畫面。程式碼如下:

用 renderer.js 產生正確的頁面內容

在 server 接收到請求後,要靠 renderer.js 去判斷當前請求的網址該顯示哪個頁面,並把 component 轉成 HTML 字串,讓 server 可以把內容回傳到 client 端顯示。程式碼如下:

這邊就是滿關鍵的地方,如果我沒有說這段程式碼是在 server 上執行的,其實也和前端沒有太大的差異。因為 NodeJS 和前端一樣都是用 JavaScript 寫的,所以一份寫好的程式碼就可以同時在前後端使用,才讓導入 SSR 變得容易。

Webpack 打包設定

最後是替 server 程式碼的打包設定了,因為和 client 的配置有一點不一樣,所以就另外寫,這邊我就不貼 Webpack 的 config 了,有興趣的話可以再去看上一篇文章。

那如果你完成了上面三個步驟,專案內容應該會和這個 GitHub 的 repository 長得差不多,我們這篇文章也會從該專案當作範例的起點,開始導入 Redux 和 SSR,建議大家可以把上方的 repository clone 下來,跟著文章每個步驟做會比較清楚哦!

導入 Redux

安裝 Redux 以及如何使用在這篇文章也不會多做說明,如果還不熟 Redux 的讀者可以先到「Redux 基礎教學」和「Redux 事件處理」看看,然後 Hooks 的寫法在「Redux Hooks 用法」。有問題再麻煩留言告訴我,感謝! 🙌

在原本的專案中,我先導入了 Redux 相關套件和 axios 來替我處理 API 請求,所以要安裝套件:

npm install axios react-redux redux redux-thunk

接著把 Redux 導入專案,讓我們可以透過 Redux 的 async action 從 API 取得資料,並且存到 store 裡。下方就依序從 action、reducer、 store 的資料夾列出程式碼內容:

要注意的是 store 就不是直接 export 建立後的 store,而是 export 一個 create store 的方法,這樣寫的原因在處理 server.js 時會提到。

接下來修改 client.js 和 pages/content.jsx,讓網站的 client 端進入到這頁的時候可以正常獲取並顯示 API 來的資料。不過這裡會拿到的是 createStore 的方法,因此使用 store 的地方要變成 createStore():

設置 SSR

Component

我們的首要目的就是要讓 server.js 知道現在要載入的頁面需不需要透過 API 獲取資料,而在上方需要打 API 的頁面是 content,因此我們稍微修改 page/content.jsx 內的幾個地方:

上方有三個修改的部分:

  1. 把獲取資料的地方拉出來建立一個 loadData 方法。
  2. useEffect 內改為呼叫 loadData 獲取資料。
  3. export 的時候不只 component,還要把 loadData 也 export 出去。

Route

加上 loadData 後,打開 Routes.js,修改關於 content 的 route 設定:

上方那樣設置是為了讓 server 抓到當前請求的 route 時,能從是否存在 loadData 去判斷需不需要透過 API 取得資料。

Server

在 server.js 上加有關 Redux 和 loadData 的邏輯:

server.js 添加的邏輯分成四個部分講解:

  1. 先是用 createStore 建立只在這一次請求用的 store,並把 dispatch 取出來,因為 server 和 client 不一樣,在 server 的資料是大家共用的,這就是為什麼要回傳 create store 的方法,而不是 store 的原因。
  2. 用 matchRoutes 方法,讓我們可以拿到對應 route 的 object array。
  3. 檢查符合的所有 route object,如果存在 loadData 的話代表需要獲取資料,就把 dispatch 丟給 loadData 讓得到的資料可以存進剛剛建立的 store 裡。
  4. 用 Promise.all 控制在所有的 loadData 都完成時,再用 renderer 產生 HTML 的 string(記得要把剛剛建立的 store 傳給 renderer 方法,待會會使用到),並丟回給前端。

renderer.js

最後不要忘記在把 component render 成 HTML 的 renderer.js 中,加上 Redux 需要的 Provider 和剛剛建立並傳進來的 store:

完成到這個步驟,先來打包前後端的程式碼,然後看看執行結果:

從上方的執行結果可以發現,在第一次載入頁面的 response 中已經有透過 API 拿到的資料了,但也能發現一件奇怪的事情,那就是雖然一開始的瞬間會顯示資料,但是後來又會清空,然後再重新顯示資料。

這是因為 SSR 在載入第一次的 response 後,就會載入 client 端的 JavaScript 程式碼,然後在 client 的程式碼中會建立一個 client 的 store,但這時候建立的 store 是沒有任何資料的,因此在這個瞬間,原本從 server 來的資料就會消失,一直到 client 的 component 載入完成,資料獲取完才會再出現資料。

要處理這個問題也很簡單,因為 Redux 的 createStore 有能設置初始 state 的功能,因此我們只需要在 server 的 response 載入到頁面的時候,先把當前頁面已處理好的 store 資料放在某個地方,然後 client 端在建立 store 時,把那些資料拿來當作初始 state 建立 store,如此一來就不會有畫面被清空的問題了。

因此再打開一次 renderer.js,把在 server 已經就位的 state 從 store 中取出,並放到全域物件 window 裡,記得要放在 bundle.js 前哦,不然 client 在建立 store 的時候就沒辦法從 window 抓已存在的 state 了:

最後修改一下 clinet.js,以及為 createStore 方法加個參數當初始 state:

調整完後再打包看看執行結果會非常有感的不同,畫面的讀取就變成「更新新獲取的資料」而不是「重新獲取新資料」:

我有把導入後的專案也上傳到 GitHub 上面囉!如果想要直接看程式碼的話也可以!

全部寫完後也總算知道為什麼當初要把 Redux 和 call API 的部分拆開來講了,如果把兩篇的文章內容寫在一起,那應該沒有幾個人可以看完 😂,不過對我自己來說,睽違了一年半再重新看這些原理,也又學到了一些新的東西!希望這篇文章也對大家有所幫助! 🙌

最後如果文章裡有任何問題的話,再麻煩各位留言告訴我,我會盡快回覆和修正的,非常感謝!🙏

參考資料

  1. Server Rendering
  2. Server-Side Rendering with React, Redux, and React-Router

--

--