Coren: React Composite Server-side Render

使用 Coren 把 Server-side Render 邏輯放到 Component 吧!

william
8 min readJun 8, 2017
from Markus Spiske on unsplash

在開始介紹我們所研發的 Coren 前我們先看看目前實作 SSR 有哪些問題

問題闡述

Server-side Render(SSR) 一直是 React 工程師很頭痛的一件事,尤其在不同的專案中所需要的 Server-side Render 邏輯就會不一樣,下面舉幾個例子。

情境 1. React-router SSR

專案使用了 React-router,要處理多個 url 的話,就要用 StaticRouter 包起來。

情境 2. React-redux SSR

如果用了Redux 跟 react-redux,SSR 就必須加入 preloadedState 的部分,require 進來的 React component 在 renderToString 前也必須用 provider 包起來。

配合 React-router 的話,有些 route 要先做 DB Query 再把資料放到 preloadedState,有些靜態的頁面卻不需要,這就必須多寫一個 function 判斷哪些 url 要什麼 data。

情境 3. Helmet SSR (HTML head)

如果需要加上 head 裡面的 title, description 或其他 metadata,就需要用 react-helmet,那 SSR 又必須加上下面這段。

情境 4. style-component SSR

最近,我們有專案使用到 style-component ,這時 SSR 又要加上下面這段才能取得 css。

舉例到這邊,你能想像要應付這麼多的狀況,一定要有一個比較有彈性的架構處理吧!

解決方案

我們團隊想出了一個有彈性的架構處理這個問題,我們決定把部分邏輯交給 Component 自己決定,也就是說

  • HTML 需要什麼 Head tag?
  • Redux 的 preloadedState 裡面放什麼資料?
  • 需要 SSR 哪些 url?

全部定義在 Component 中,舉例來說:

Head SSR 解決方法

SSR 需要知道 render 這個 React App 需要放什麼 Head 的話,Component 中可以定義如下。

User Component 在 defineHead 定義 head 中需要 user 2 這樣的 title

Redux SSR 解決方法

那 preloadedState 有辦法做 DB Query 再放進去嗎?可以的!

Product Component 定義要先到 DB fetch products 後再放到 state

React Router SSR 解決方法

有辦法知道 Component 需要 render 哪些 URL 嗎? 可以的!

About Component 需要 render “/about”

那如果像是商品頁面,為了效能考量,一次可以 render 多個 HTML 嗎?你可以這樣寫:

先去 DB 要了多筆資料後,搭配 /products/:id 產生多個 URL 做 SSR.

這樣不但清楚明瞭,哪天哪個 Component 不用了也不用去改 SSR 的程式,直接拿掉就好.

到這邊你一定會有疑問,那這些 define 開頭的 static method 該如何取得 return 的資訊呢?

所以我們團隊開發了一套框架 Coren 處理這個問題。

Coren

Coren, A Composite Renderer,幫助你用上述的方式產生出 HTML

我們現在用個簡單的範例網站來解釋如何用 Coren

這個網站簡單的分成三個路徑

  • / 會到首頁
  • /users 會列出所有的使用者
  • /users/:id 會顯示一個使用者相關資訊

各位可試著按照 repo 中的 Usage 部分把網站架起來用用看.

那這個專案 SSR 的時候,是怎麼使用 Coren 的呢?

我們先從 Home 這個 Component 開始看

  • 2–4行:這邊 import coren 的 collector 這個 decorator,並包在 Home 外面,這邊需要注意的是,所有有 define method 的 Component 都需要使用這個 decorator
  • 6–11行:defineHead 定義了title, description 所需的資訊
  • 13–15行:defineRoutes 定義了要 render “/”

接著,SSR 要怎麼蒐集到這些資訊呢? 我們交給 Collector ( 搜集者),把資訊都搜集到 SSR 處理.

Collector 是什麼?

Collector 會針對特定的 define method,取得回傳的資料

舉例來說,HeadCollector 會搜集 defineHead 的回傳.

這邊需要解釋的是,當 Component 在 SSR 的過程中

  • import 的時候,會 trigger componentDidImport
  • constructor 執行的時候,會 trigger componentDidConstruct

藉著這兩個 lifecycle ,collector 可以在特定 lifecycle 搜集回傳值,以 HeaderCollector 來說,由於 react-router render的時候,只有 match 到的 component 會被 trigger componentDidConstruct,而有被 render 的 component 回傳的 head 才需要放到 HTML 中.

componentDidConstruct 的會照著 constructor 的順序被呼叫,所以 parent component 會較早被呼叫到.

我們把拿到的 title 跟 description 都 push 到 array 中,最後 SSR 在執行 appendToHead,也就是要 insert DOM 到 HTML 時,把最上層的 Parent 的head 放進去.

接著我們看 /users 列出使用者的部分,這個路徑會 render UserList 這個 Component

我們擷取部分程式碼看一下

值得注意的部分是 12–22 行這邊,definePreloadedState 會從 server 端拿到 DB 這個變數,就可以先 find users 在把 state 回傳

最後 SSR 會透過 ReduxCollector 取得 definePreloadedState 回傳的 promise,過程如下:

ReduxCollector 透過 componentDidImport 拿到每個 component 的 state 後,會在 appWillRender 這個階段等每個 promise 都完成.

這邊解釋一下 appWillRender 這個部分.

SSR 在呼叫 collector 的資料時,也是有不同的階段

  1. SSR 先 import app, trigger componentDidImport
  2. 開始 ReactDOM.renderToString 前,呼叫 appWillRender 等待 collector 把事情完成
  3. 接著看有哪些 URL 要 render,每一次都會呼叫以下的 method
  4. Collector.routeWillRender: 代表某個 route 要被 render
  5. Collector.wrapElement: 以 ReduxCollector 為例,需要用 Provider 把 App ReactElement 包起來,並回傳 ReactElement
  6. ReactDOM.renderToString
  7. Collector.appendToHead
  8. Collector.appendToBody
  9. 最後完成多次 4–8 的 loop 後,回傳一個 array,一個 route 對應一個 HTML

對應到 ReduxCollector:

  1. componentDidImport 搜集 promise
  2. appWillRender 等待 promise 完成
  3. wrapElement 把 app 包起來同時,取得 redux store 回傳的 state
  4. appendToHead 時,insert state 到 HTML 中

最後一個 User Component 以上內容都有包含到,這邊我們先略過,有興趣的讀者可到 User 看.

Serverside Render

最後,我們來 Serverside render,詳細的程式碼在 /server/ssr.js

  • 13–15 行,我們 create 從 coren require 進來的 App,把 react app 的路徑傳進去
  • 17–28行,我們註冊多個Collector,這邊的 ImmutableReduxCollector 只是多包一個 Immutable 在 state 外面,不太清楚 immutable 的讀者可以先不用理會
  • 31–34行,我們 create 一個 multiRoutesRenderer,把 react js bundle 的路徑傳進去
  • 最後 25 行的 ssr.renderToString() 產生多個 HTML 後,把 HTML serve 產生到 filesystem,接著 serve 給使用者看.

到這邊我們的 HTML 就產生出來了!

結論

我們團隊目前先把比較簡單的部分整理成 Collector,接著我們會支援更多的功能上去,Coren 可以運用在很多情境,例如,我們團隊會把幾個專案 SSR 的 HTML 放到 CDN 上保證一定的 performance,但用來產生部落格的 static page 也是非常適合的情境.

如果有什麼意見,歡迎分享給我們!

你的意見會讓我們變得更好!

--

--