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

神Q超人
Starbugs Weekly 星巴哥技術專欄
11 min readMar 2, 2020
Photo by Kelly Sikkema on Unsplash

Hi!大家好,我是神 Q 超人。事情是這樣子的,SSR 一直都是我滿想要了解的技術之一,於是在過年的時候我就找了幾天來玩一下 React 的 SSR 框架 Next.js,一開始覺得很有趣,但做到一半的時候

我整個就不爽了!(注一)

難道說要使用 SSR 就一定得靠 Next.js 嗎?這樣子就算把教學文件全都看完,我也還是不曉得 SSR 是怎麼辦到的,我的 SSR 裡面沒有靈魂。所以就想說,不如再多花一點時間,從頭了解在 React 中如何實現 SSR!也才有這篇文章的誕生,但其實原理都是一樣的,希望可以拋磚引玉帶給各位一些幫助 🙌

SSR

關於 SSR 網路上應該找得到很多文章解釋,我也會把很多很棒的文章列在延伸閱讀,這裡我就簡單提一下。

平常我們在做 React 專案,都會先準備一個 HTML 檔,裡面可能長這樣子:

<html>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>

而當網頁載入時,因為只有一個 div 節點,其他什麼都沒有,所以畫面會先是空白的,等到我們打包專案產生的 bundle.js 被載入後,才會由 Router 去判斷要 Render 什麼畫面和執行 API 獲取資料等等,就是 CSR(Client-Side Rendering)。

而在 SSR(Server-Side Rendering)中,我們就沒有那個 HTML,

第一次的畫面也就不是那個只有 div 的頁面,而是從 Server 端來的,

Server 會用 URL 的 path 來決定要 Render 什麼畫面,然後顯示在前端頁面上,顯示完第一次的頁面後,仍然會把 bundle.js 下載下來,靠它處理接下來的操作和 Render。

所以在 SPA 中 SSR 和 CSR 的不同就只有在第一次的畫面是由誰 Render 而已

SSR 的優缺點

優點的部分

  1. 主打 SEO,因為 CSR 不管輸入什麼,網頁一開始都是空的,很多搜尋引擎會不曉得你的網站裡面有什麼,就無法讓使用者透過關鍵字找到。但 SSR 在第一次載入時就會有內容在了,搜尋引擎表示非常滿意。
  2. 使用者看到第一個畫面的時間會比 CSR 快,效能也會更好,因為不需要再等 bundle.js 載入才開始 Render,而且第一次 Render 的所有事情(包含 call API)都是 Server 做的,Client 端就比較沒有負擔。

缺點的部分

  1. 學習門檻是要稍微了解 NodeJS,畢竟在 SSR 中 Server 是絕對必要的。如果還不會也沒關係,文章中會簡單的解釋如何實作。
  2. 因為第一次的 Render 是在 Server 上運行,所以如果有在 Component 或是其他運行到的地方使用到 window 的話就會出錯,這裡可以參考 這個 issue 的討論內容作為解決方案。

實作 SSR

在實作前,先說明一下本篇文章中只會就基本的功能來實作,至於 call API 和 Redux 的部分怕一次塞太多東西不好理解,之後會再寫另一篇新文章來解釋 😃

那現在既然要實作 SSR,至少也要有個專案可以讓我導入對吧?非常剛好,這裡就有一個「basic-csr-of-react」,沒錯就像他的名字一樣,就是個基本的 React CSR 專案,功能也很簡單,只有另外用到 react-router 來轉換兩個頁面:

接下來就將 SSR 導入吧!

用 NodeJS 建立 Server

就如同一開始說的,SSR 沒有初始的 HTML 了,取而代之的是 Server 回傳的初始畫面,所以我們使用 Node.JSexpress 來做 Web Server。

先下載 express:

npm install express --save

之後在 src 的目錄下建立一個新的檔案,叫做 server.js,並使用 express 建立 Web Server:

上方分成四點講解一下:

  1. Server 啟動時的 port 號,指定為 3001。
  2. express 用來載入靜態檔案的位置,因為我們會在第一次回傳的資料上把前端打包好的程式 bundle.js 給寫在 script 裡面,所以指定 dist,也就是 Client 檔案所在的目錄(官方 API 說明文件)。
  3. app.get 會依照指定的 URL (第一個參數)下去執行第二個參數中的 function,這裡我們的 URL 是 *,所以所有的 URL 都會跑進來,而 function 中有兩個參數,URL 的路徑會在 req.path。然後裡面的 res.send 能夠送入 HTML 的字串,輸出給前端顯示。
  4. 最後把 Server 啟動囉!

製作一個 Sevrer 就是這樣子輕輕鬆鬆,但我們還得處理需要輸出的 HTML。

初次載入頁面

在這個階段,我們要在 src 目錄下建立一個新的目錄叫做 helpers,並在 helpers 內新增一個 renderer.js,裡面會是一個 function,會回傳初始頁面的內容,但這裡有幾點和我們平常的 Render 不一樣:

  1. 在 CSR 裡會用 ReactDOM.render 來將 DOM Render 到當前畫面的指定節點,但是 SSR 是跑在 Server 端,所以就不能這樣做。取而代之的是我們可以將要顯示在畫面上的 Component,用 ReactDOM/server 提供的renderToString 轉成字串。
  2. SSR 一開始會先從 Server 端丟出一個靜態的文件內容顯示在畫面上,隨後 bundle.js 就載入了,所以在 SSR 裡直接用 StaticRouter 來取代 HashRouterBrowserRouter,至於路徑的部分會從 req.path 中取得。關於更多 StaticRouter 的內容可以參考 官方文件

所以這個 renderer.js 的 function 需要接收 req,讓 Router 判斷需要 Render 哪個 Component,整個 function 會長這樣:

完成 renderer.js 後我們回到 ./src/server.js,將 app.send 替換成 renderer.js 處理後的初始頁面:

Webpack 打包設定

恭喜到最後一個部分了,接下來要處理打包,在 ./webpack.config.js 裡面有做 Client 的打包設定,但現在多了Server 就得再另外寫 webpack 的打包檔。

於是我先將原本的 ./webpack.config.js 名字改成 ./webpack.client.js,之後再複製一份取名叫做 ./webpack.server.js,Client 那邊的設定就不動了,下方專心來改 Server 的部分。

首先 module 裡面的 loader 設定不用修改,因為都是對 React 和 JavaScript 做編譯,但我們要編譯的檔案不是跑在 Browser 的 Client,而是要跑在 Node 上面的 Server,所以要加上 target,並指定為 node。然後 entry 裡面的入口文件要改成 ./src/server.js,至於 output 的部分為了和 Client 做出區別,所以把打包後的檔案指定到 ./build 目錄。

還有一點不同是在 Node 環境下,就不需要再打包第三方的依賴 Library 了,因為在上 Server 的時候仍然可以讓它再 npm i,所以可以使用 webpack-node-externals 在打包時先隔離掉 node_modules,用法如下:

先從 npm 下載:

npm i webpack-node-externals --save

把 webpack-node-externals 加到 externals 後,Server 的 Webpack 設定檔內容會如下:

打包一波

完成上方那一切後,就可以來打包試試囉!先打開 package.json 加上 scripts 指令:

"scripts": {
"build-client": "webpack --config webpack.client.js",
"build-server": "webpack --config webpack.server.js",
"build": "npm run build-client && npm run build-server",
"server": "node build/bundle.js"
}

輸入 npm run build 進行打包:

完成後,檔案應該會出現在 ./dist 和 ./build 的資料夾內,確認沒問題就能用 npm run server 啟動 Server:

啟動後就能夠以 http://localhost:3001/ 進入網站,如果順利的話網頁應該會呈現:

當我一開始重新整理的時候,第一次的頁面 Preview 就已經回傳了有內容的初始頁面,而接著在 bundle.js 載入後,當我點擊到 Other page 再點回 Home,就不會再經過 Server 了。

因此 SSR 和 CSR 只有一開始的 Render 不同而已!bundle.js 會在初始頁面後被載入,接手運作使用者在畫面上的行為。

文中導入 SSR 的專案範例會放到 GitHub 上面,如果有問題再麻煩留言告訴我,非常感謝!

總結

做了什麼

最後複習一下我們做了什麼:

  1. 搭建 Server,讓他可以回傳 HTML 的文字給 Client。
  2. 創建 renderer,可以依照 req.path 產生對應的 HTML 字串。
  3. 增加一個負責打包 Server 的 Webpack 設定檔。

That’s all!不管今天寫的是 React 或是 Vue,原理都是一樣,只要能夠讓 Server 丟出第一次的畫面,然後載入 Client 的程式碼 bundle.js 就好。

個人覺得很厲害的地方是除了有使用到 Browser 提供的 API 外,原本在 Client 的程式碼幾乎可以重用,所以 SSR 也被稱作 Isomorphic JavaScript,意指 Server 和 Client 可以共用同一段程式碼(建議大家可以看看 這篇文章解釋)。

優化 SSR 及開發體驗

本文中的內容盡量以簡單且易懂的方式,讓人瞭解 SSR 的原理,所以其實還有很多地方可以做優化,以下就列出幾點,如果有興趣的朋友們可以試著加上他們:

  1. 在 Client 中的 ReactDom.render 可以改成 ReactDOM.hydrate文件說明兩者的差異
  2. Webpack 的設定檔其實可以把相同的部分,寫在一個 webpack.base.js 裡面(例如 module),並用 webpack-merge 將設定檔合併,這麼一來 Server 或 Client 的設定檔就都不會有重複的內容了。
  3. 如果覺得用 NodeJS 開發,都要一直 build 然後重新整理,可以考慮裝一下 nodemon

注釋專區

注一:這是知名 Youtuber 狠愛演 的官方起手套路,然後我真的沒有在業配!但是如果是六萬多人親自看見,那「椒~聯絡我們 😉」(這也是梗啦 XD)

其實我真的沒有想要打那麼長的,但是不知不覺就打了一大堆內容,希望自己可以很簡單的將 SSR 的實作闡述清楚,也讓大家能覺得 SSR 其實並不難,因為好多文章都講得滿複雜的,導致我搞了幾個小時才弄懂 😭,最後也推薦
react-ssr-news
這個 Project,我是照著它的內容做才恍然大悟的!大家也可以閱讀看看!

那如果文章中有任何問題或是有錯誤的地方,再請麻煩留言告訴我!感激不盡 🙌

參考文章

  1. React 中同构(SSR)原理脉络梳理
  2. Server-Side Rendering with React, Redux, and React-Router
  3. 一看就懂的 React Server Rendering(Isomorphic JavaScript)入門教學

--

--