在開始介紹我們所研發的 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 中可以定義如下。
Redux SSR 解決方法
那 preloadedState 有辦法做 DB Query 再放進去嗎?可以的!
React Router SSR 解決方法
有辦法知道 Component 需要 render 哪些 URL 嗎? 可以的!
那如果像是商品頁面,為了效能考量,一次可以 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 的資料時,也是有不同的階段
- SSR 先 import app, trigger componentDidImport
- 開始 ReactDOM.renderToString 前,呼叫 appWillRender 等待 collector 把事情完成
- 接著看有哪些 URL 要 render,每一次都會呼叫以下的 method
- Collector.routeWillRender: 代表某個 route 要被 render
- Collector.wrapElement: 以 ReduxCollector 為例,需要用 Provider 把 App ReactElement 包起來,並回傳 ReactElement
- ReactDOM.renderToString
- Collector.appendToHead
- Collector.appendToBody
- 最後完成多次 4–8 的 loop 後,回傳一個 array,一個 route 對應一個 HTML
對應到 ReduxCollector:
- componentDidImport 搜集 promise
- appWillRender 等待 promise 完成
- wrapElement 把 app 包起來同時,取得 redux store 回傳的 state
- 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 也是非常適合的情境.
如果有什麼意見,歡迎分享給我們!
你的意見會讓我們變得更好!