Server-Side Rendering :SSR 是一種前端框架能夠在後端渲染出HTML的能力。那些能夠在客戶端和服務端完成渲染的應用就叫做universal app
為什麼需要SSR?
為了理解為什麼需要SSR,這裡我們需要了解下web應用在過去十年內的發展史。SSR與SPA(Single Page Application)的興起緊密相連。與傳統的服務端渲染的app相比,SPA在速度和使用者體驗發麵存在很大的優勢。
但是使用SPA有個問題,通常情況下使用者第一次請求會返回一個空html檔案和一堆JS和CSS連結,渲染html之前會先把JS和CSS提前下載下來。
這就意味著首次渲染的時候,使用者必須要等更長的時間。同時對於爬蟲來說,解析到的頁面也是一個空頁面。
因此SSR的主要思想就是首次在server端渲染應用,後續可以充分利用SPA的優勢,在客戶端完成渲染。
SSR + SPA = Universal App
有的文章中也會把Universal App講成isomorphoic app,實際上這兩個是同一個東西。採用SSR的情況下,首次渲染的時候,使用者不需要等JS載入完成後在看到渲染完成的頁面,而是在請求返回的時候就已經拿到渲染完成的頁面了。
對於使用slow 3G的使用者來說,使用SSR會大大改善使用者體驗。使用者將會直接看到網頁內容而不是等待20s+才能看到網頁內容。
現在情況下,所有傳送到server端的請求都會被直接返回成HTML。這樣對於做SEO的部門來說也是十分有利的。
對爬蟲來說不會區別對待SPA引用和其他靜態站點,同樣會為服務端渲染的內容生成索引。
簡而言之,使用SSR有兩點好處:
- 首次渲染速度更快
- 生成的HTML內容可以被索引到。
一步步來理解SSR
下面筆者將通過一個例子,一步步來實現一個完整的SSR案例。首先從React的服務端渲染API開始,後續每一步我們都將加入一些新的內容。
可以follow 這個專案倉庫 ,每一步的程式碼都會有一個tag,讀者可以通過git checkout tags/xxx -b xxx
的方式獲取每一步的程式碼(xxx為對應的tag名)。
Basic Setup
開始介紹SSR之前,我們需要一個server。這裡筆者採用express來渲染React應用。
在程式碼的第10行,我們用express啟動了一個靜態伺服器。同時我們也建立了一個用於處理非靜態請求的handle函式。非靜態的路由將會返回HTML程式碼。
在程式碼的第13~14行,我們用renderToString
函式把一開始的JSX程式碼轉換成字串,這段字串後續將被插入到HTML模板中。
PS:我們並沒有直接啟動sever.js ,而是通過index.js來啟動server.js。在index.js中,我們用babel外掛來抹平client和server端的差異,保證client和server都能夠使用es module和jsx。
在SSR中client端的程式碼也需要從ReactDOM.render
的改成ReactDOM.hydrate
。這個函式將會接受服務端渲染的react程式碼並掛載事件處理函式。
想看到完整的例子,可以check react-ssr tag為basic的程式碼。到這兒為止,我們就完成了一個簡單的服務端渲染的react app。
React Router
到目前為止,我們的應用實際上啥事也沒幹。現在我們來往之前的應用加入一些路由。先來看看如何處理服務端部分:
現在Layout
元件在client端上將會渲染出路由元件。對應的我們需要在server端模擬出client的路由實現。下面我們列出server
端程式碼的修改部分:
在服務端的程式碼中,我們需要把React Application包裝在StaticRouter
元件中,並提供location
引數。
PS:context
用於在渲染React DOM
的過程中追蹤可能的重定向請求:比如client需要根據3XX響應重定向。
完整的案例需要checkout tag為router
的程式碼。
Redux
在專案已經具備路由能力的情況下,下面我們來整合redux
。一些場景下,我們需要使用redux來管理client端的狀態。但是在服務端渲染的情況,如何根據當前狀態來渲染部分DOM是個問題,因此我們有必要在服務端初始化redux。
如果應用在服務端dispatch action的情況下,SSR需要記錄下這些操作,並把最終的state和HTML一起返回給client。在client端,會把服務端返回的state設為redux的初始狀態。
我們先來看看server端的實現:
這段程式碼看起來實現的十分醜陋,但是我們確實需要把服務端渲染出來的redux狀態和HTML程式碼一起返回給client。
接著來看看client部分的實現:
這裡我們呼叫了兩次createStore
,一次在server端,一次在client端。但是在client端上需要把server端儲存下來的狀態設為redux的初始狀態。
完整的例子可以看當前專案的redux
tag。
Fetch Data
最後一步就是載入資料。這是個比較棘手的問題。我們從一個返回JSON資料的介面開始講起。
在程式碼倉庫中,筆者通過開放API獲取了2018第一賽季的Formula的資料。我們希望在Home頁面顯示所有的Formula資料。
我們可以在所有React app
掛載完成、所有元素都已經渲染完畢的的情況下呼叫API介面來完成需求。如果這樣的話,可能會存在一些loading畫面,對於使用者體驗並不友好。
考慮到專案中已經整合了Redux
,我們可以通過Redux
來儲存資料並返回給前端的方式來載入資料。
如何在server端呼叫API介面、將介面返回資料儲存在Redux中並讓客戶端根據相關資料來渲染HTML呢?
那麼需要呼叫哪些介面呢?
首先我們需要通過一種不同的方式來申明路由。所以我們把路由改成如下所示:
同時我們也需要在元件上宣告所有的資料:
PS: fetchData
是一個 Redux thunk action
,dispatch fetchData的時候會返回一個promise
。
同時在服務端,我們也用了一個react-router
中的特殊的函式:matchRoute
:
通過這個方法,當服務端根據當前URL渲染頁面的時候會得到需要被mounted
的元件。我們會收集所有元件需要的資料,等待所有介面都已經返回資料,並把獲取的資料塞到redux中才會繼續執行服務渲染。
切換到tag為fetch-data
的分支可以看到整個案例。
從這兒開始,我們就會開始從各個維度進行比較,並比較出哪些場景適合使用SSR哪些場景不適合使用SSR。比如說對一個電商app來說,獲取所有的產品是重中之重,但是價格以及一些其他的邊欄filter相比之下就顯得不那麼重要。
Helmet
最後我們來看看SEO。當和React打交道的時候,我們經常需要在<head>
標籤中設定不同的值。比如:title
、meta tags
、keywords
等等。
記住<head>
標籤中的內容一般不是React App
的一部分。
react-helmet就是為了解決修改<head>
標籤中的內容而生的,並對SSR提供了良好的支援。
你可以在元件樹中的任何地方加入head標籤內的資料。在client端上,react-helmet提供了一種修改React App
以外部分的能力。
我們也在SSR中加入這種能力:
現在我們已經顯示了一個具備基礎功能的React服務端渲染的案例。我們從一個返回HTML內容的express應用開始,慢慢加入了路由、狀態管理以及獲取資料的能力。最後我們還處理React App
之外的部分。完整的程式碼在master分支可以看到。
Conclusion
正如本文所示,SSR並不適合一件難事,但是SSR也可以做的很複雜。如果一步一步來實現需要會更容易些。那麼專案中是否需要加入SSR呢?具體情況具體分析。如果網站訪問量很大,則建議做SSR。但是如果你的應用是類似於工具或者dashboard這種應用,則沒必要花費較多的精力來實現SSR。