揭開React中server-side rending的神祕面紗

原文地址:https://medium.freecodecamp.o...

image
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+才能看到網頁內容。
image
現在情況下,所有發送到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應用。
image

在代碼的第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代碼並掛載事件處理函數。
image

想看到完整的例子,可以check react-ssr tag爲basic的代碼。到這兒爲止,我們就完成了一個簡單的服務端渲染的react app。

React Router

到目前爲止,我們的應用實際上啥事也沒幹。現在我們來往之前的應用加入一些路由。先來看看如何處理服務端部分:
image

現在Layout組件在client端上將會渲染出路由組件。對應的我們需要在server端模擬出client的路由實現。下面我們列出server端代碼的修改部分:
image

在服務端的代碼中,我們需要把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端的實現:
image

這段代碼看起來實現的十分醜陋,但是我們確實需要把服務端渲染出來的redux狀態和HTML代碼一起返回給client。
接着來看看client部分的實現:
image

這裏我們調用了兩次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呢?
那麼需要調用哪些接口呢?
首先我們需要通過一種不同的方式來申明路由。所以我們把路由改成如下所示:
image

同時我們也需要在組件上聲明所有的數據:
image

PS: fetchData 是一個 Redux thunk action,dispatch fetchData的時候會返回一個promise
同時在服務端,我們也用了一個react-router中的特殊的函數:matchRoute
image

通過這個方法,當服務端根據當前URL渲染頁面的時候會得到需要被mounted 的組件。我們會收集所有組件需要的數據,等待所有接口都已經返回數據,並把獲取的數據塞到redux中才會繼續執行服務渲染。
切換到tag爲fetch-data的分支可以看到整個案例。

從這兒開始,我們就會開始從各個維度進行比較,並比較出哪些場景適合使用SSR哪些場景不適合使用SSR。比如說對一個電商app來說,獲取所有的產品是重中之重,但是價格以及一些其他的邊欄filter相比之下就顯得不那麼重要。

Helmet

最後我們來看看SEO。當和React打交道的時候,我們經常需要在<head>標籤中設置不同的值。比如:titlemeta tags keywords 等等。
記住<head> 標籤中的內容一般不是React App的一部分。
react-helmet就是爲了解決修改<head>標籤中的內容而生的,並對SSR提供了良好的支持。
image

你可以在組件樹中的任何地方加入head標籤內的數據。在client端上,react-helmet提供了一種修改React App以外部分的能力。
我們也在SSR中加入這種能力:
image

現在我們已經顯示了一個具備基礎功能的React服務端渲染的案例。我們從一個返回HTML內容的express應用開始,慢慢加入了路由、狀態管理以及獲取數據的能力。最後我們還處理React App之外的部分。完整的代碼在master分支可以看到。

Conclusion

正如本文所示,SSR並不適合一件難事,但是SSR也可以做的很複雜。如果一步一步來實現需要會更容易些。那麼項目中是否需要加入SSR呢?具體情況具體分析。如果網站訪問量很大,則建議做SSR。但是如果你的應用是類似於工具或者dashboard這種應用,則沒必要花費較多的精力來實現SSR。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章