一個簡易的預渲染自動骨架屏方案
前言
我們都知道,目前傳統的SPA 網頁在完成腳本加載後,通常還需要進行接口請求,拿到遠端數據後才能進行完整地內容呈現
而在接口請求的過程中,為了過渡無數據的空白場景,並提示用戶“數據請求中”,常用的方法為做一個loading 動畫效果
而在用戶胃口越來越刁的今天,一個簡單的loading 效果已經不太能安撫用戶了,而骨架屏就是一種安撫用戶的進階方案
最終成品鏈接(懶人用): auto-skeleton-plugin
什麼是骨架屏?
簡單來說,骨架屏就是在還未產生可閱讀內容時,先將網頁的大致結構框架呈現給用戶,以達到安撫用戶等待過程中的不耐煩心理、提升用戶存留的效果
骨架屏的實現,通常有兩種方式
- 手動書寫骨架
- 自動生成骨架
手動寫骨架的方式,好處是可以做出高定制性的骨架效果,缺點是開發成本大,效率低,但本文不對此方式進行展開
那麼如何實現自動骨架屏的效果呢?一個簡單的方式是:將已有內容的樣式進行調整,生成對應的骨架效果,例如以下代碼,可以將所有文字內容,變成骨架條塊
functiongenerateSkeleton(){//文字節點;[...document.querySelectorAll('*')].filter((node)=>!['script','style','html','body','head','title'].includes(node.tagName.toLowerCase())).map((node)=>[...node.childNodes].filter((node)=>nodeinstanceofText)).flat(Infinity).forEach((node)=>{letspan=document.createElement('span')node.parentNode.insertBefore(span,node)span.appendChild(node)span.style=`background: #f2f2f2;color: transparent !important;`})}
這樣,只要我們完善不同內容如圖片、圖標等元素的骨架化過程,就可以得到一個相對可用的內容骨架化效果了
自動骨架化的好處是,生成骨架的效率高,開發成本很低,但缺點是定制性相對較差,需要根據已有內容來確定骨架效果
但這有一個問題,我們期望是在應用剛打開時,還未請求數據前就呈現骨架,目前顯然是做不到的
而我們可以藉助“預渲染”來實現期望的效果
什麼是預渲染?
預渲染類似服務端渲染,它的過程大概是這樣的:在應用完成打包後,立刻啟動一個headless瀏覽器進行頁面訪問,再將訪問的結果輸出成html文件的渲染過程
通俗地說就是:打包完後本地先訪問看一看,看到啥就“截個屏”存起來,然後輸出一個html 文件,覆蓋原本構建生成的index.html
這樣,用戶訪問打包好的index.html 時,看到的就是一個有內容的網頁
那麼,借助預渲染,我們可以將上述自動骨架屏的過程,放在headless瀏覽器加載出網頁內容後,具備內容後再將內容骨架化,再輸出成html,就可以實現用戶訪問時,還未請求數據前,先呈現骨架的效果
自動骨架屏的過程實現
我們可以參考一個常用的預渲染的webpack插件prerender-spa-plugin來實現這個過程
查閱源碼可知,這個插件並未實現核心渲染過程,其實只是將prerenderer包裝成了webpack插件的形式,並承擔了將最終結果輸出成html產物文件的功能
關鍵源碼: https:// github.com/chrisvfritz/ prerender-spa-plugin/blob/master/es6/index.js#L65-L70
...constPrerenderer=require('@prerenderer/prerenderer')...functionPrerenderSPAPlugin(...args){...constafterEmit=(compilation,done)=>{constPrerendererInstance=newPrerenderer(this._options)PrerendererInstance.initialize().then(()=>{returnPrerendererInstance.renderRoutes(this._options.routes||[])})...}...}...module.exports=PrerenderSPAPlugin
prerenderer承擔的則是使用headless瀏覽器訪問網頁,並輸出訪問結果的功能,其官方內置了兩種可選的headless瀏覽器: puppeteer和jsdom
由於puppeteer需要下載的內容較大,我們考慮使用較輕量的jsdom來完成這個效果
在翻閱了部分renderer-jsdom的源碼後,可以找到headless瀏覽器採集網頁內容的部分
我們只需要在採集網頁內容前,對內容進行骨架化,就可以得到期望的效果
constJSDOM=require('jsdom/lib/old-api.js').jsdom...constgetPageContents=function(window,options,originalRoute){...returnnewPromise((resolve,reject)=>{...functioncaptureDocument(){//此處可在輸出html結果前,先對網頁內容進行骨架化// generateSkeleton就是上邊咱們整理出來的dom操作實現自動骨架化過程generateSkeleton(window)constresult={...html:serializeDocument(window.document)}...returnresult}...}...}classJSDOMRenderer{...asyncrenderRoutes(routes,Prerenderer){...constresults=Promise.all(routes.map(route=>limiter(()=>{returnnewPromise((resolve,reject)=>{JSDOM.env({url:`http://127.0.0.1:${rootOptions.server.port}${route}`,...})}).then(window=>{returngetPageContents(window,this._rendererOptions,route)})})))...returnresults}...}module.exports=JSDOMRenderer
至此,簡易自動骨架屏效果的方案已經敘述完成,整個過程,需要我們自己動手的主要是骨架化過程的部分,其餘之處,都可通過參考已有過程實現來完成,那麼具體過程實現,此處就不再繼續展開了,動手能力強的小伙伴,大概可以自己一把梭出來
結尾
預渲染方案待展開的功能還是有不少的,例如
- 如何內聯樣式? (這條比較容易做到,借助jsdom 自身的resourceLoader 足矣)
- 如何保留關鍵樣式,去除無用樣式? (有一定難度,可參考uncss ,配合postcss實現)
- 預渲染性能是否充足,能否用來做SSR? (jsdom渲染速度較快,此處進行了實踐santi )
以下是上述方案的自動骨架插件實現,目前自動骨架化的過程比較簡陋,只具備了基礎的可用性,也希望能得到大家的幫助,共同完善自動骨架化的過程