前端同構渲染的思考與實踐

開篇

前端同構渲染的相關架構,給我最直觀的感覺,這是前端渲染最爲複雜的一種方案,也是爲了追求極致的用戶體驗不得不去作的一種嘗試,雖然 Node.js 的引入賦能了傳統前端領域、SEO 優化也再也不是個問題,但很明顯,這些只是副產品。html

問題

上帝爲了咱們開了一扇窗,同時也會爲咱們關上一扇門。前端

咱們所知的傳統型 SPA,單頁面應用,貼近用戶端越近,交互越複雜,它的弊端就越明顯,在咱們享受 JavaScirpt 給咱們帶來的無刷新體驗和組件化帶來的開發效率的同時,『白屏』這個隨着 SPA 各類優勢隨之而來的缺點被遺忘,咱們擁有菊花方案在 JavaScript 沒有將 DOM 構建好以前蒙層,擁有白屏監控方案將真實用戶數據上報改進,但並無觸碰到白屏問題的本質,那就是『DOM 的構建者是 JavaScript,而非原生的瀏覽器』。vue

<html>
  <head><title /></head>
  <body>
  	<div id="root"></div>
    <script src="render.js"></script>
  </body>
</html>
複製代碼

如上代碼,在 SPA 架構中,服務器端直接給出形如這樣的 HTML,瀏覽器在渲染 body#root 這個節點完成以後,頁面的繪製區域其實仍是空的,直到 render.js 構建好真實的 DOM 結構以後再 append 到 #root上去。此時,首屏展現出來時,必然是 render.js 經過網絡請求完畢,而後加上 JavaScript 執行完成以後的。node

讓咱們回到最初的那個前端時代,那時候 JavaScript 尚未那麼強大,咱們的服務器端所有吐出 HTML 給前端,咱們使用 jQuery 解決用戶的交互,這種方式雖有不少弊病,但不能否認的是擁有理論上最低白屏時間。webpack

<html>
	<head><title /></head>
  <body>
  	<div id="root">
    	<div class="header">
        <img src="logo.png" />
      </div>
      <div calss="content">
        <div class="shopitem">
        </div>
      </div>
    </div>
  </body>
</html>
複製代碼

如上代碼,在直出的服務器渲染中,瀏覽器直接拿到最終的 HTML,瀏覽器經過解析 HTML 以後將 DOM 元素生成而進行渲染。因此相比於 SPA,服務器端渲染從直觀上看:ios

  • 轉化 HTML 到 DOM,瀏覽器原生會比 JavaScript 生成 DOM 的時間短
  • 省去了 SPA 中 JavaScript 的請求與編譯時間

**web

解決

Node.js 的出現極大程度的給傳統前端賦予了更大的能量,前端的分離也從前期的物理文件的區分轉變爲職責上的區分,前端開發者從頁面仔的噩夢中解脫出來,最重要的是,JavaScript 能在服務器端執行了。在享受這些紅利的同時,咱們就會不自覺的設想一種方案,它擁有 SPA 的大部分優勢,卻解決了它大部分的缺點,那就是服務器端輸出 HTML,而後由客戶端複用該 HTML,繼續 SPA 模式,這樣豈不是既解決了白屏和 SEO 問題,又繼承了無刷新的用戶體驗和開發的組件化嘛。redis

嗯,若是這樣的話,就會有個一致性的問題。咱們必須在瀏覽器端複用服務器端輸出的 HTML 才能避免多套代碼的適配,而傳統的模板渲染是可行的,只要選擇一套同時支持瀏覽器和 Node.js 的模板引擎就能搞定。咱們寫好模板, 在 Node.js 準備好數據,而後將數據灌入模板產出 HTML,輸出到瀏覽器以後由客戶端 JavaScript 承載交互,搞定。axios

軟件開發中遇到的全部問題,均可以經過增長一層抽象而得以解決後端

思路到了這裏,咱們就會發現,『模板』實際上是一種抽象層,雖然底層的 HTML 只能跑在瀏覽器端,可是頂層的模板卻能經過模板引擎同時跑在瀏覽器和服務器端,此爲垂直方向,在水平方向上,模板將數據和結構解耦,將數據灌入結構,這種灌入,實際是一錘子買賣,管生無論養。

隨着時間的推動,組件化的大潮來了,其核心概念 Virtual DOM 依其聲明式和高性能讓前端開發者大呼爽爽爽,但究其本質,就是爲了解決頻繁操做 DOM 而在 HTML 之上作的一層抽象,與模板不一樣的是,它將數據與結構產生交互,有表明的要數 Facebook 方使用的單項數據流和 Vue 方使用的 MVVM 數據流,大道至簡,咱們觀察函數 UI = F(data), 其中 UI 爲最終產出前端界面,data 爲數據,F 則爲模板結構或者 Virtual DOM,模板的方式是 F 只執行一遍,而組件方式則爲每次 data 改變都會再執行一遍

因此理論上,不管是模板方式仍是組件方式,先後端同構的方案都呼之欲出,咱們在 Node.js 端獲取數據 ,執行 F 函數,獲得 HTML輸出給瀏覽器,瀏覽器 JavaScript 複用 HTML,繼續執行 F 函數,等到數據變化,繼續執行 F 函數,交互也獲得解決,完美~~~

實施

但因爲組件化大勢所趨,下文將略去模板方案,咱們以 Vue 爲類比,下圖代表其實施思路:

通用代碼

因爲 F 同時須要在瀏覽器端和服務器端執行,因此對於整個 Vue App,咱們須要同時支持兩端,也就是通用代碼。因此咱們須要將 SPA 架構的代碼進行改造:

  • 分爲兩個入口,分爲服務端和客戶端,只引入通用代碼,而後在不一樣的環境裏調用各自的渲染函數。固然,在客戶端 ReactDOM.render 會生成 DOM 結構,而服務器端經過 ReactServer.renderToString 將生成 HTML,須要由 HTTP Server 推給前端,各入口處解決特異的環境問題;
  • 通用代碼中不可在不斷定執行環境的狀況下引用 DOM、調用 window、document 這些瀏覽器特異和引用 global process 這些服務器端特異的操做,這每每是引發 Node.js 服務出問題的根本緣由;
  • 爲了兼容兩端,在選擇庫時,須要也同時須要支持兩端,好比 axios,lodash 等;
  • React 和 Vue 都有生命週期,須要區分哪些生命週期是在瀏覽器中運行,哪些會在服務器端運行,或者是同時運行,如使用 Redux 或者 Vuex 等庫,最好在組件上引入 asyncData 鉤子進行數據請求,同時供兩端使用;
  • 斷定不一樣的執行環境能夠經過注入 process.env.EXEC_ENV 來解決,形如:
if (process.env.EXEC_ENV === 'client') {
  window.addEventListener(...);
}

if (process.env.EXEC_ENV === 'server') {
}
複製代碼

構建與運行

  • 在使用 webpack 進行構建時,須要將公共 App 部分打包出來,造成公共代碼,由服務器端引入執行,而客戶端能夠引用打包好的公共代碼,再用 webpack 引入以後進行特異處理便可;
  • 須要引入 Node.js 中間層,負責請求數據,提供渲染能力,提供 HTTP 服務,因爲 HTML 模板須要在服務端引入,CDN 文件須要自行處理;
  • 至於 babel 的使用,能夠在瀏覽器中通用處理,服務端只解決特殊語法,如 jsx,vue template;

新世界

至此,白屏問題問題看起來是解決了,經過把 JavaScript 的渲染邏輯放到 Node.js 端進行,咱們加快了首屏出現的時間,可是聯想到 Node.js 對前端的賦能,咱們或許能夠作的更多。

再議首屏

讓咱們把視角移動的更細緻一些,關注『從服務器端輸出 HTML』這一部分,其隱藏的含義是咱們須要把 App 渲染的全部 HTML 都輸出給前端,其實否則,舉個栗子:

好比在移動端有一個頁面,它有大約 10 屏的高度,若是咱們在服務器端所有輸出 10 屏實際上是有點浪費的,咱們能夠只輸出首屏須要的,從而下降 render 執行時間從而下降 TTFB 時間,讓頁面更快的到達用戶眼前。實踐中,通常狀況是輸出大概快兩屏的樣子,就能處理因此機型的高度問題,剩下的 8 屏,在瀏覽器端繼續渲染,漸進產出內容,用戶無感知。

資源控制

得益於 Node.js 輸出 HTML 的另外一層含義,就是咱們能夠直接在首次接觸就能感知到客戶端,也就有了足夠的靈活性,再舉個栗子:

有個針對安卓平臺和 iOS 平臺不一樣的腳本只要加載,若是在 SPA 狀況下,只有等 JavaScript 執行時咱們斷定 navigation.userAgent 來獲知先在是哪一個平臺,而後在 appendChild 一個 script 到 body,但若是服務端能首次接觸就能感知,咱們能夠在服務端直接拿到 HTTP 請求中的 userAgent 斷定平臺,根據標識在模板中處理,很顯然,這樣很穩。

另外,若是有一些特別複雜的計算,服務端能夠有更多的辦法將數據更快的處理,以免繁忙無比的瀏覽器接手。

緩存控制

通常的業務場景下,咱們須要在 Node.js 中經過內網將數據獲取到,而後經過 render 函數渲染出 HTML(通常須要將數據附帶給 HTML 輸出以便重複利用),這個時候咱們能夠經過頁面訪問地址和生成的 HTML 字符串作緩存策略,在緩存(通常選擇 redis 等方案)以後,下次直接將一樣的頁面直接輸出到前端,可大幅提升渲染性能

但這種方案也有不少限制,由於要考慮頁面地址、多平臺下、帳戶是否登陸,頁面是否須要改動等狀況:

  • 頁面地址緯度,在不一樣的地址下,HTML 輸出不一致,因此 URL 可做爲 key 的元素之一;
  • 未登陸態,頁面能夠直接緩存,如需斷定平臺特異,需在 Node.js 端進行處理;
  • 已登陸態,若是已緩存某一個已登陸用戶的 HTML,須要將跟登陸相關的組件抹去從新換掉,或者直接給予未登陸態頁面,在客戶端進行變動。

挑戰

同構渲染看似美好,但其相對傳統 SPA 確有着更多挑戰:

Node.js

服務器端渲染相對應傳統的 Node.js 應用,renderToString 函數不只 CPU 密集,並且不一樣的組件對機器資源的要求不盡相同,這就更須要 Node.js 指標的監控、日誌的記錄、錯誤的收集、崩潰機制的完善。這裏額外的關鍵的指標是 renderToString 的時間,它反應了 Node.js 渲染所使用的時間,若是加入緩存機制,就須要統計命中率等等。

代碼質量

關於寫通用代碼,要求比 SPA 架構對開發者提出了更高的要求,咱們須要當心再當心,由於萬一搞錯,將致使很難排查的內存泄露和 CPU 飆升,而且一旦出了問題,就像要修理天上跑的飛機同樣,很是困難。還記得有一次在相似 componentWillMount 寫了一些跟瀏覽器相關的代碼致使的內存飆升,還有一次 JSON.stringify 一個大對象致使的 CPU 飆升,不堪回首。這方面 alinode 作的很好,確實能夠知足這種飛機場景。

結語

爲了效率, 前端們付出了艱辛的努力,不管是工程上咱們想方設法的製造工具,仍是組件化的引入,咱們解決的是開發的效率,而不管是 Virtual DOM 的引入解決頻繁操做的 DOM,仍是用了提高用戶體驗而使用的 SPA 架構,咱們解決的是用戶的使用效率,是前端的性能。而同構渲染也是這樣一種方案,它引入了 Node.js 的複雜度,要求咱們寫出限制更多的代碼,其根本目的仍是爲了讓用戶更快更早的看到頁面,那怕是 50 毫秒,那怕是 10 毫秒。


關於咱們

咱們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業羣。咱們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。咱們支持了阿里集團幾乎全部的保險業務。18年咱們產出的相互寶轟動保險界,19年咱們更有多個重量級項目籌備動員中。現伴隨着事業羣的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入咱們~

咱們但願你是:技術上基礎紮實、某領域深刻(Node/互動營銷/數據可視化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。

若有興趣加入咱們,歡迎發送簡歷至郵箱:luguang.ylg@antfin.com


本文做者:螞蟻保險-體驗技術組-月影

掘金地址:楊柳岸醬

相關文章
相關標籤/搜索