由于汽車業務的特殊性,天貓汽車基于 Rax 多頁應用自建了商品詳情的 H5 頁面。自定義商詳承載了眾多業務能力和投放場景。隨著業務的發展和頁面承載內容的增多,開始出現白屏時間太長等體驗問題。
前端性能優化算是個老生常談的問題,我們的頁面已經做過首屏接口合并、圖片懶加載、骨架屏等體驗優化,想進一步提升用戶體驗就要從渲染機制和渲染容器入手了。從容器側看,淘寶端原生提供的 pha 容器提供的數據預請求、資源離線緩存等功能,可以有效提升手淘內的 H5 頁面體驗。但是其它渠道的端缺少類似容器能力,需要從渲染機制方向尋找出路。
SSR(Server Side Render)是相對于現有渲染機制CSR(Client Side Render)的一種渲染方案。在用戶通過客戶端(Client Side)請求頁面資源時,CSR 拿到是一份空文檔,再通過數據請求、執行渲染腳本后生成文檔,最后展示給用戶。而 SSR 在請求后拿到的就是服務端(Server Side)經過數據請求腳本渲染以后的完整文檔,由于它不強依賴客戶端的能力,具有更加穩定的性能和較好的用戶體驗。
為了提升淘寶外、特別是中低端機用戶的瀏覽體驗,我們對自定義商詳進行了 SSR 化探索,完成了 Rax 多頁應用向 Rax 全棧應用的改造,以下是我們的改造歷程。
代碼結構
項目現在的 Rax 多頁應用體系和目標體系 Rax 全棧應用都基于 Rax 框架,目錄結構比較相似,全棧應用在多頁應用的基礎上增加了服務端渲染函數和FaaS相關 工程配置。Rax 多頁應用的目錄如下:
├── src
│ ├── app.json # 路由及頁面配置
│ ├── components/ # 自定義業務組件
│ ├── pages/ # 頁面
├── build.json # 工程配置
├── package.json
└── tsconfig.json
Rax 全棧應用目錄如下:
├── src
│ ├── apis # 函數源碼
│ │ ├── configuration.ts
│ │ ├── lambda/ # 接口
│ │ ├── render # 渲染函數目錄
│ │ │ ├── home/ # 渲染函數,需與 pages 里的頁面名一一對應
│ │ │ └── ....../
│ │ └── typings/ # 數據類型定義
│ ├── pages # 頁面
│ │ ├── home/
│ │ └── ....../
│ ├── document/ # 文檔結構
│ ├── app.json # 路由及頁面配置
│ └── typings.d.ts
├── build.json # 工程配置
├── f.yml # 函數平臺配置
├── midway.config.ts. # midway 配置,主要指定接口和渲染目錄
├── package.json
└── tsconfig.json
從目錄來看,承載頁面渲染的核心業務邏輯如 pages、components 都無須改動。SSR 模式下,服務端返回的不再是空文檔,而是經過一次渲染后的文檔框架,所以需要保持代碼在 Node 環境下可運行。由于汽車商詳在 Rax 多頁應用開發時沒有這種環境約束,因此對技改提出了環境模擬的需求,這點在后面會著重提到。
數據請求
CSR 模式下,進入頁面后拉取主接口數據,執行 js 完成頁面渲染。SSR 下,主接口數據需要在服務端獲得,完成服務側的文檔渲染。
客戶端得到的只是一個干文檔,需要再次執行一遍 js 以激活文檔的事件監聽、狀態傳遞等,成為可交互的頁面,這個過程也有一個形象的名稱,叫注水(hydrate):
?請求流程的轉變
由于請求時機的改變,請求及其前后邏輯需要移至服務端執行。
汽車商詳基于自建的一套端到端的渲染方案(XRoot)進行接口設計和頁面渲染。接口數據中是組件集,包括各組件名稱和對應組件需要的 props。我們封裝了一套 XRoot 組件,自動執行接口請求、數據注入、頁面渲染等工作。
在商詳首頁中,該接口請求的工作并不只是數據拉取與注入,還承擔了一系列請求前后的業務邏輯(如設置全局變量、容災處理等)。對其中邏輯進行抽象歸納,將業務邏輯聚合到 beforeRequest 和 afterResponse 中:
SSR 模式下,主接口請求在服務端執行,其執行邏輯為:
afterResponse
邏輯中有一部分是對數據本身的處理,留在服務端實現。另一部分是 UI 相關的,需要在客戶端 Hydrate 階段再次執行。
?中間層網關的模擬實現
在阿里集團內,移動端接口和后端的交互會經過了一層 MTOP 網關。MTOP 網關提供了協議解析、安全防護、穩定性保障等能力。SSR 模式下,服務端的數據請求無法經過這層網關,而是直接訪問后端 API。
此時業務依賴到的相關 MTOP 能力需要業務側在 SSR 下手動補齊,其中直接影響頁面渲染的就是 MTOP 對接口中空數據的處理:
原始接口數據中值為 null
的屬性都被 MTOP 層處理過了,如果 SSR 不做空值屬性的刪除,前端的默認取值就會出問題:
const { text, tip = '默認提示' } = tagsList[0];
console.log(tip); // 非空處理前:null ;非空處理后:'默認提示'
這里寫一個簡單的工具函數來做這件事:
export const deleteNullProperties = (obj: Object | Array ) => {
const memory = new Set();
const fn = (obj) => {
if (memory.has(obj)) return obj;
if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj))) {
for (const [key, value] of Object.entries(obj)) {
if (value === null) {
delete obj[key];
} else {
obj[key] = fn(value);
}
}
}
memory.add(obj);
return obj;
}
return fn(obj);
}
?
另外,MTOP 網關還提供了用戶登陸態的解析與傳遞,SSR 側也需要業務自行從 cookie 中讀取用戶登陸信息。
環境模擬
一般來說,由于服務端和瀏覽器端環境的差異,在做前后端同構應用的時候,開發者都會自覺注意環境差異,避免在不恰當的時機訪問瀏覽器對象,導致 SSR 側報錯。
但是由于本次是改造項目,歷史項目只關注 CSR 場景,不可避免地充斥著預期以外的 DOM/BOM 訪問,此時有以下幾種解決方案:
從改造成本和維護成本來看,借助框架能力和引入社區方案是兩種可以低成本探索的思路。
?框架能力
Rax 全棧應用框架本身提供了在服務器端模擬瀏覽器環境的能力,從而來盡可能保證 SSR 和 CSR 編碼的一致性。其環境變量模擬的基本原則是:
-
所模擬的信息可由服務端數據推導得出,例如 location、navigator。
-
所模擬的信息不會引起代碼執行邏輯的錯誤,例如對 localStorage 的模擬。
從中可以看出,框架模擬能力并不旨在進行環境模擬,相反它還在試圖為頁面提供一些真實有用的信息。因此框架提供的模擬能力相對有限,一些常用方法也沒有空方法占位,如試圖調用 window.addEventListener 時會報 undefined 錯誤。
由于 Hydrate 階段的存在,我們的目的只是希望代碼能夠在服務端不報錯,客戶端側的二次渲染會提供更加準確的信息,框架能力并不能滿足需要。
?jsdom-來自社區的服務端環境庫
jsdom 是許多 Web 標準的純 JavaScript 實現,特別是 WHATWG DOM和HTML標準。該項目并不是為了模擬服務端環境而存在,它的主要目標是模擬足夠多的 Web 瀏覽器子集,使得頁面可以在服務端運行,從而測試和抓取真實世界的 Web 應用程序。 ?它對瀏覽器環境強大的模擬能力可以幫助我們避免對項目源碼的改造。 由于 jsdom 同樣追求對功能性的模擬,功能強大的同時,在服務端運行時可能出現預料之外的問題,需要詳盡測試。在使用 jsdom 時,也遇到了一些版本兼容問題,可以通過降低庫版本和動態加載等方式解決。
其他問題
?降級策略
Rax 全棧應用框架采取中間件降級策略,SSR 側有任何報錯都會自動將空文檔返回給客戶端,由客戶端發起數據請求和渲染,即降級為 CSR。
降級邏輯
降級中間件源碼
業務側也可以根據需要使用框架提供的 useCSRRenderer 鉤子函數進行主動降級:
?自閉合標簽問題
習慣了 react 的 JSX 語法體系,我們通常會寫出許多自閉合標簽以提高可讀性和代碼整潔性:
// 自定義組件的自閉合
// 原生標簽的自閉合
但事實上 HTML 規范并不支持對 的解析,以一個簡單的 demo 為例:
在開發者的期待里,b 和 c 同級,d 是 c 的子標簽。但實際渲染結果中,c 成了 b 的子級。 對這種情況的一種表象理解是,瀏覽器把自閉合標簽當作正常
頭處理,為其匹配了一個
。到最后閉合標簽不夠用了,瀏覽器自動加了兩個
為什么在 JSX 中寫自閉合標簽能夠得到期待中的結果呢?事實上是 CSR 框架側幫我們做了這件事,如 react 在其官方文檔里就有提到:react地址:https://react-cn.github.io/react/tips/self-closing-tag.html
從實踐來看,Rax 框架在 CSR 側的渲染同樣做了類似的處理,但是 Rax 全棧應用的服務端渲染沒有,其 SSR 側生成的文檔只是簡單將自閉合標簽原樣返回:
不同階段下的文檔結構
由于在 hydrate 階段瀏覽器判斷 ssr 文檔與客戶端預期不一致時,會被丟棄掉重新渲染,所以這種處理沒什么功能性問題。只是這樣就失去了 SSR 的首屏優化功能。在 Rax 團隊完善自閉合標簽問題之前,業務側需要避免寫出非自定義標簽的自閉合。
?與 CSR 倉庫的功能同步
因為我們是對 CSR 項目的改造,為了不影響線上功能,另啟了一個應用倉庫。隨著改造的進行,原 CSR 項目也一直在接新需求不停迭代。 借助git merge --allow-unrelated-histories
機制,我們在代碼同步方面不必投入太多成本。但是為一個業務場景維護兩套應用,其中需要付出的溝通、維護、測試等成本還是很高。
?下一步計劃
如前所述,兩個應用的維護成本較高。因為兩者的框架基礎是一致的,我們就希望將兩個應用合并起來,至少能降低一點代碼同步和溝通成本。與技術支持同學溝通了一下,發現沒有這個先例,可能會有很多意料之外的問題,所以這部分仍在試錯中。
小結
借著本次服務端渲染(SSR)改造升級的實踐,我對前端的渲染機制也有了更深入的理解。雖然 SSR 相較于 CSR 有著許多優勢,卻未成為 Web 渲染的主流模式,是因為構建 SSR 應用并不輕松:需要熟悉服務端開發、了解服務端和客戶端的環境差異、服務部署、服務器運維等,對前端開發提出了更高的要求。
而云原生基礎設施的蓬勃發展、Nodejs FaaS 函數服務建設的逐漸完善,都將大大降低 SSR 應用的接入門檻。前端開發者將只需關注業務邏輯就能輕松獲得 SSR 能力,用戶也更有機會得到更好的交互體驗。
審核編輯 :李倩
-
數據
+關注
關注
8文章
7081瀏覽量
89181 -
函數
+關注
關注
3文章
4338瀏覽量
62746 -
代碼
+關注
關注
30文章
4802瀏覽量
68743
原文標題:天貓汽車商詳頁的SSR改造實踐
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論