大家好,我是若川(點這里加我微信?ruochuan12,長期交流學習)。今天推薦這篇圖文并茂的SSR技術文章。這是江西前端群里一個小伙伴的文章。群里小伙伴很多都在知名大廠,但他們都很低調。
點擊下方卡片關注我、加個星標,或者查看源碼等系列文章。學習源碼整體架構系列、年度總結、JS基礎系列
SSR最佳實踐
秒開率對于用戶的留存率有直接的影響,數據表明, 網頁加載時間過長會直接導致用戶流失.轉轉集團作為一家電商公司, 對于H5頁面的秒開率有著更加嚴格的需求, 在主要的賣場側頁面(手機頻道頁、3c頻道頁、活動頁)等重要流量入口我們都采用了SSR(服務端渲染)技術來構建頁面,今天就帶大家了解一下我們摸索出來的一些最佳實踐.
網頁的前世今生
在早期的web應用中,實際上我們都是用的服務端渲染技術, 像jsp、asp、php等各種后臺模板生成的頁面,前端都是拿到整張頁面,不用自己去拼接DOM.后來隨著前后端分離開發模式,衍生出了最主要的兩種渲染方式CSR以及SSR.
CSR : 客戶端渲染,整個渲染流程是: ?瀏覽器請求url --> 服務器返回index.html(空body、白屏) --> 再次請求bundle.js、路由分析 --> 瀏覽器渲染bundle.js體積越大, 就會導致白屏的時間越長,給用戶的體驗就越差(當然,這可以借助打包構建工具來優化這部分)
交互流程圖如下
SSR : 服務端渲染, ?服務端渲染分為兩個步驟:
階段一: ?瀏覽器請求url --> 服務器路由分析、執行渲染 --> 服務器返回index.html(實時渲染的內容,字符串) --> 瀏覽器渲染 ?(此時是一個靜態頁面, 不可交互, 依賴于服務端的能力, 這就是快的原因)
階段二:瀏覽器請求bundle.js --> 服務器返回bundle.js --> 瀏覽器路由分析、生成虛擬DOM --> 比較DOM變化、綁定事件 --> 二次渲染 (用戶可交互)
我們來看看整個交互流程圖 :
SSR構建邏輯
理解了兩種渲染模式的異同,我們來看看SSR整個構建邏輯(主要以Vue-SSR為例)
我們以官網的圖片為例:

從圖中我們可以知道 :
在整個構建過程中, 我們有兩個入口, 一個是server-entry.js, 執行server端的邏輯, 一個是client.js, 執行client端的邏輯, 然后通過會將webpack打包分成兩個Bundle: 服務端bundle; 客戶端bundle. Node.js會處理服務端bundle用于SSR, 客戶端bundle會在用戶請求時和已經由SSR渲染出的頁面一起返回給用戶, 然后在瀏覽器執行”注水”(hydrate
), 接管Vue接下來的業務邏輯.
理解了整個構建邏輯,接下來我們來看看我們是怎么運用SSR來服務我們的項目的.
SSR構建項目的背景
賣場側業務首頁組成大同小異: 主要分首屏和第二屏, 首屏有多個模塊組成, 第二屏是商品Feed流,便于讀者理解, 我們抽象出了頁面結構圖:
(歡迎大家下載轉轉app體驗)
而且這些頁面都有一個共同的特點:
只用于展示, 和用戶的狀態不強綁定(不需要用戶登錄)
頁面狀態穩定,內容不會經常變更
都是重要流量的第一個入口(首頁)
由于對于秒開率有著極高的要求,又承載了主要流量入口,結合以上頁面特點,所以我們使用了SSR來提升用戶體驗.
經過一系列的探索和探究, 我們最終使用Nuxt.js來作為我們的技術選型.
這里提下為啥使用Nuxt.js作為我們的技術選型, 主要原因有以下幾點:
集團內部C端業務是以Vue技術棧為主,B端技術棧是以React為主, 所以不考慮React服務端渲染技術棧;
Nuxt.js是開箱即用的服務端渲染框架,不用開發人員自己去搭建Vue+ Vue-server-renderer + vuex來集成服務端渲染框架, 接入成本比較低.
SSR運用的最佳實踐
目前我們使用SSR實現的主要能力有:
首屏使用服務端渲染,第二屏使用客戶端渲染,首屏模塊數據可調整,即優化了性能, 又豐富了頁面配置.
合理化使用緩存, 進一步提升用戶體驗.
實現css注入,達到按需換膚的效果.
使用ErrorBoundary攔截錯誤,使得組件錯誤不會影響整個頁面白屏.
按需加載第二屏數據,只有滑動到可見范圍, 才加載第二屏數據
針對大促場景, 結合服務端能力, 以及各種監控,docker擴容,保證頁面穩定.
接下來就和大家探索其中幾種能力的主要思路:
怎樣實現首屏使用服務端渲染,第二屏使用客戶端渲染
這種實現方式主要是結合asyncData在服務端異步獲取數據,使用vue動態組件component的特性,來調整模塊的渲染順序; mounted生命鉤子只會在客戶端執行, 使用僅在客戶端渲染組件的特性來實現的.
示例代碼:
<template><!--服務端渲染,動態獲取首屏模塊并且加載對應模塊的數據,?使用error-boundary來攔截錯誤--><template?v-for="(e,?i)?in?structureOrder"><error-boundary><component?:info="activityState.structure[e]":is="Mutations.name2Component(e)"class="anchor":id="e":key="i"?:name="e"?v-if="activityState.structure[e]?||?e?===?'bar'?"/></error-boundary></template><!--客戶端渲染--><client-only><!--滑動到可見范圍加載對應的數據--><div?:is="listComponent"?:tab="labelFilter"/></client-only>
</template>
獲取數據:
//服務端渲染數據
async?asyncData({app,?route,?req})?{const?initData?=?await?app.$axios.$get(host,?{params:?{name:?key,?from,?smark,?keys:?`structure,base,labelFilter,navigate,redPack,${elements}`},?headers})const?{structureInfo,?structureOrder,?restStructure,?anchors}?=?Mutations.initStructure(initData)return?{structureInfo,restStructure,structureOrder,?//動態返回對應模塊的名稱useVideo:?Mutations.checkUseVideo(req),theme,pageFrom:?route.query.from,isPOP,anchors,...formInfo}
},
async?mounted()?{//獲取客戶端渲染的數據const?res?=?await?this.initData()
},
怎樣使用ErrorBoundary捕獲組件級別錯誤,避免整個頁面白屏
關于ErrorBoundary這個捕獲錯誤的組件,這個組件的主要功能是使得組件級的錯誤不會蔓延到頁面級,不會造成整個頁面的白屏,考慮到服務端渲染可能會發生偶發性錯誤,狀態容易變的不可控, 所以使用這個能力還是很有必要的, 這個組件主要使用vue提供的 errorCaptured 來捕獲組件級的錯誤, 想詳細了解這個api的作用可以去看官方文檔,具體的實現如下:
const?errorBoundary?=?Vue?=>?{Vue.component('ErrorBoundary',?{data:?()?=>?({?error:?null?}),errorCaptured(err,?vm,?info)?{this.error?=?`${err.stack}\n\nfound?in?${info}?of?component`SentryCapture(err,?1)?//異常上報到sentryreturn?false},render()?{return?(this.$slots.default?||?[null])[0]?||?null}})
}//?全局注冊errorBoundary
Vue.use(errorBoundary)
怎樣實現css注入,實現頁面換膚
這個功能的主要作用是 : 可以根據配置json文件定制化活動頁面的樣式, 做到"千人千面" (一個會場的key可以配置一種樣式, 但是底層代碼是一套),使得元素多樣化,在視覺上給用戶體驗帶來很大提升.
我們先來看看效果示意圖:
以上就是展示效果, 借住CSS注入, 我們可以根據不同的json文件來定制化頁面的樣式, 只需要維護一套代碼, 簡單高效.
實現邏輯也很簡單,主要是運用了Nuxt.js框架提供的head方法:
head()?{//this.baseInfo.additionStyle是從json文件拿到的樣式//通過css權重,?可以實現樣式覆蓋return?{style:?[{cssText:?this.baseInfo?.additionStyle?||?'',?type:?'text/css'}],__dangerouslyDisableSanitizers:?['style']??//?防止對一些選擇器的特殊字符進行轉義}},
不僅如此, 還可以實現js注入, 感興趣的小伙伴可以自己去了解,底層原理可以了解下 vue-meta 這個庫
但是, 隨著業務的不斷迭代, 這種注入方式還是存在很多可優化的點:
每個活動頁運營同學都要維護一份json文件,里面包含冗長的css配置字段, 活動規則等等, 特別是css配置字段,就是一段冗長的css, 給運營和開發同學帶來很大的不便;
運營同學維護成本高,學習成本高,操作成本高;
對于UI同學成本也大, 每次都需要UI同學來設計活動頁面樣式;
目前, 集團內部正在使用 魔方 一步一步去替代這種方式, 魔方 只需要運營同學拖拖拽拽, 就能生成一個活動頁, 簡單高效, 想要了解魔方的同學, 可以繼續關注我們的公眾號
怎樣實現組件滑動到可見范圍,才加載數據
其實這種優化頁面的方法并不是說只適用于SSR, 其他非SSR頁面也可以使用這種方式來優化;
看看我們的實現方式 :
function?asyncComponent({componentFactory,?loading?=?'div',?loadingData?=?'loading',?errorComponent,?rootMargin?=?'0px',retry=?2})?{let?resolveComponent;return?()?=>?({component:?new?Promise(resolve?=>?resolveComponent?=?resolve),loading:?{mounted()?{const?observer?=?new?InterpObserver(([entries])?=>?{if?(!entries.isIntersecting)?return;observer.unobserve(this.$el);let?p?=?Promise.reject();for?(let?i?=?0;?i?<?retry;?i++)?{p?=?p.catch(componentFactory);}p.then(resolveComponent).catch(e?=>?console.error(e));},?{root:?null,rootMargin,threshold:?[0]});observer.observe(this.$el);},render(h)?{return?h(loading,?loadingData);},},error:?errorComponent,delay:?200});
}export?default?{install:?(Vue,?option)?=>?{Vue.prototype.$loadComponent?=?componentFactory?=>?{return?asyncComponent(Object.assign(option,?{componentFactory}))}}
}
實現原理主要是使用vue高階組件, 元素到達可見范圍內, 延遲加載組件;
看看效果圖:

我們可以看到,只有到底部商品Feed流出現在可視范圍,才去請求對應的接口
針對大促場景怎樣保證頁面穩定
所謂大促場景,是指像 6.18, 雙11,這種場景下, 面對大流量, 如何保證頁面穩定? SSR是CPU密集型任務, 意味著很耗費服務器資源,集團目前主要采取的策略是:
對接口進行壓測, 模擬高并發場景下頁面性能, 接口響應速度;
集團內部實現了一套監控系統, 可以實時監控CPU,內存的消耗情況;
需要有服務器擴容方案, 比如接入docker, 可以實現服務器實時擴容
怎樣利用緩存
請大家移步集團一位前端大佬寫的公眾號文章: Nuxt實現的SSR頁面性能優化的進一步探索與實踐
最終,看看我們最終的實現效果:

可以看到, 首屏渲染時間在594ms, 秒開率在百分之87左右;
SSR的不足
ssr的使用過程并不是一帆風順的, 在使用的過程中, 也總結幾點不足之處:
對于開發人員的要求更高, 要學習其他的額外知識,例如: Linux , node相關知識, 需要具備一定的后端思維;
服務端渲染接口抓包不方便, 我們在客戶端抓取不到服務端的接口請求, 不過對于使用Mac電腦開發的同學,可以使用 proxychains-ng 來抓取服務端請求的接口
冗長的配置環境過程, 每次開發聯調需要配置后端host
對于服務器資源有要求,并發量越大, 資源消耗的越多
服務端渲染可能會發生偶發性錯誤, 需要有一套降級方案
至于如何取舍, 看各位同學的項目需求,以及運用場景;
總結
SSR的使用有利有弊, 我們應該結合自己的業務特性去制定合適的方案, 它的優點就是快, 有利于SEO, 缺點也很明顯, 比較耗費服務器資源, 對于億級流量的超巨app來說, 理論上是不太合適的, 集團內部也有自己的一套方案來優化客戶端渲染, 使得用戶體驗盡量向SSR靠齊.每一種技術的運用只有實踐了才知道利弊,才能產生碰撞. 本文只是簡單的帶大家了解一條業務線上對SSR的運用, 所闡述的方面也只是冰山一角, 希望給廣大開發者帶來一定的啟發, 前人栽樹, 后人乘涼, 感謝轉轉FE前輩們留下的寶貴財富.
參考資料
nuxt官網: ?https://www.baidu.com/link?url=xy0d8KPUgTmiVoGge6g-FgdeqjJSTjxdpT0tpxZzBG_&wd=&eqid=db50cacf00052e5e000000066081587d
Vue SSR指南: https://ssr.vuejs.org/zh/guide/
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你進群。
·················?若川出品?·················
今日話題
寫篇原創優質文章不容易,寫了文章特別希望讓更多人看到,寫過文章的都懂。就像我現在每篇文章都帶上源碼系列鏈接,也是希望更多人看到。江西前端群里小伙伴寫了這篇文章,我連忙聯系開了白名單轉載。公眾號平臺保護原創作者權益非常好,如果你看到一篇文章被轉載多次,大概率說明這篇文章確實不錯。歡迎分享、收藏、點贊、在看我的公眾號文章~
一個愿景是幫助5年內前端人走向前列的公眾號
可加我個人微信 ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還能加我進模擬面試群)
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
點擊上方卡片關注我、加個星標,或者查看源碼等系列文章。
學習源碼整體架構系列、年度總結、JS基礎系列