一種對開發更友好的前端骨架屏自動生成方案
- 2019 年 10 月 3 日
- 筆記
(馬蜂窩技術原創內容,公眾號 ID:mfwtech)
一份來自 Akamai 的研究報告顯示,在對 1048 名網購戶進行採訪後發現:
-
約 47% 的用戶期望他們的頁面在兩秒之內載入完成。
-
如果頁面載入時間超過 3s,約 40% 的用戶會選擇離開或關閉頁面。
一直以來,為了提升用戶在頁面載入時的體驗,無論是 Web 還是 iOS、Android 的應用中,前端開發工程師都做了許多工作。除了解決如何讓網頁展現速度更快的問題,還有很重要的一點就是提升用戶對載入等待時間的感知。「菊花圖」以及由其衍生出的各種載入動畫就是一類常見的解決方案,相信無論是開發者還是用戶對下面這個圖標都不會陌生:
本文要介紹的「骨架屏」則被視為菊花圖升級版的方案。受現有骨架屏方案的啟發,馬蜂窩電商前端研發團隊實現了一種自動化生成骨架屏的方法,並在馬蜂窩商城的多個頁面中實現應用,取得了不錯的效果。
一、什麼是骨架屏
骨架屏可以理解為在頁面數據尚未返回或頁面未完成完全渲染前,先給用戶呈現一個由灰白塊組成的當前頁面大致結構,讓用戶產生頁面正在逐漸渲染的感受,從而使載入過程從視覺上變得流暢。生成後的骨架屏頁面如下圖所示:
骨架屏的主要優勢為:
- 用戶避免看到長時間的白頁
- 可以獲知頁面的大體結構,減小用戶認為頁面出錯而離開的機率
- 與菊花圖相比視覺更加流暢
二、常見的前端骨架屏方案
在選擇骨架屏之前,我們也考慮了一些其他的方法,比如能否通過服務端渲染(SSR)的方式來避開前端白屏時間的問題。但發現需要涉及項目過多,還會涉及服務的構建與部署;或是通過 prerender-spa-plugin 提供簡單的預呈現,它對 SPA 支援友好,但需要額外的 webpack 配置,且因為包源的問題,下載時間過長,有時還會莫名失敗,等等,都因為種種原因最終放棄。
經過一系列調研後,我們對業界常見的幾種骨架屏解決方案,以及它們的優勢、不足進行了一個簡單的梳理。
1. UI 骨架屏圖
即通過 UI 提供符合頁面首頁樣式的圖來充當骨架屏,將骨架屏 base64 圖片插入 root 根節點,在 webpack 打包時嵌入項目中。
這是一種簡單粗暴的方法,實現起來比較容易。但缺點也很明顯,就是需要 UI 設計師支援和開發介入,不能自動生成。
2. 手寫骨架屏
即通過手寫 HTML、CSS 的方式為目標頁訂製骨架屏。這種方式可以做到對頁面真實樣式的復刻。不過一旦由於各種原因導致頁面樣式發生改變,就需要再改一遍骨架屏的樣式和布局,極大增加了維護的成本。
3. 自動生成靜態骨架屏
目前比較受關注的是餓了么開源的插件 page-skeleton-webpack-plugin,其具體實現原理為:
- 生成骨架屏
通過 Puppeteer 操控 handless Chrome 打開需要生成的骨架屏頁面,在等待頁面載入完成之後,保留頁面布局樣式的前提下,通過對頁面中元素進行增刪,對已有元素通過層疊樣式進行覆蓋,使其展示為灰白塊。然後將修改後的 HTML 和 CSS 提取出來,將頁面分為不同的塊區域,例如文本塊、圖片塊、按鈕塊、SVG、偽類元素塊等,分別對每個塊進行處理,使其盡量與原頁面保持一致。這裡用到了 Puppeteer page 實例的 addScriptTag 方法來將處理塊的腳本插入到 headless Chrome 打開的頁面當中。
實際生成的骨架屏頁面與原頁面可能還會存在差距,插件通過 memory-fs 將骨架屏寫入記憶體中,可以通過預覽頁面對生成的骨架屏進行二次編輯和效果預覽,修改完成後點擊生成按鈕就能生成一份新的骨架屏寫入到項目中。
借一張圖來說明:
- 插入骨架屏
骨架屏的 DOM 結構和 CSS 通過離線生成後,在構建時注入模板 (EJS) 中的節點下面,插入到 HTML 是在 after-emit 鉤子函數中進行。
page-skeleton-webpack-plguin 生成骨架屏的方案可以根據項目中不同的路由頁面生成相應的骨架屏頁面,並將骨架屏頁面通過 webpack 打包到對應的靜態路由頁面中。
它的不足之處在於:
-
實際使用過程中無法監聽介面返回導致生成骨架屏的時機是否準確
-
生成的頁面與業務人員寫的結構品質有直接關係,經常出現需要手工二次調整的情況
在這樣的背景下,馬蜂窩電商研發前端團隊希望找一種在提升用戶體驗的同時,對開發更友好的骨架屏生成方式,能針對不同的業務場景自動生成出相似的骨架屏,並且實現自動注入。對於開發而言,只需要執行一條命令,或者簡單配置,就可以生成骨架屏,不需要再考慮後續的維護工作。
在方案調研過程中,draw-page-structure 為我們的設計提供了靈感。
4. draw-page-structure
- 生成骨架屏:
根據 URL 指定的線上地址,配合 Puppeteer 獲取當前頁面的 DOM 結構,並對其中元素節點生成骨架屏文件到 filepath 指定的文件裡面,就可以生成骨架屏頁面,結果如下圖所示:
-
插入骨架屏
將上述生成的骨架屏文件插入到頁面根節點下面一般為 id=”app” 的節點,然後在通用工具里提供主動銷毀骨架屏的方法,就可以幫助開發主動控制或銷毀骨架屏,顯示頁面真實內容。
draw-page-structure 的設計思想很大程度上可以滿足我們的需求,不足的是只能對線上已經存在的 URL 生成骨架屏,不支援開發環境。另外由於是自動生成,當頁面存在重定向(如果未登錄重定向到登錄頁面)的情況時,生成的骨架屏可能與預期不一致。而且它的內部實現並不完善,可能導致某些結構複雜的頁面下生成的骨架屏需要二次優化調整。
於是,我們開始了進一步的探索。
三、對開發更友好的實現方案
1. 設計思路
基於對現有方案的借鑒,我們想到了在配置文件中指定要生成骨架屏的頁面 URL 和文件輸出的目錄,運行時讀取配置文件中的配置項,通過 Puppeteer 打開指定的頁面並注入 evalDom.js 的方法。因為此 JS 是在 Puppeteer 裡面執行的,所以可以獲取到當前頁面完整的 DOM 結構,這給我們留下了非常大的發揮空間。
最初我們是從獲取到的 DOM 結構中的 body 標籤出發,遞歸去處理頁面上的所有節點,處理完成後用生成的 DIV 替換原有元素的位置。第一版方案中通過 getBoundingClientRect 和 getComputedStyle 的方法來獲取元素所有計算屬性和相對於視口的寬高和位置,然後結合元素本身的樣式屬性遞歸渲染,保留頁面原始 DOM 嵌套層次。
但由於能夠決定元素位置的屬性實在太多,如 position,z-index、width、height、top、display、box-sizing、flex 等都需要考慮,導致無法聚焦對頁面 DOM 結構處理的邏輯,而且這些屬性在處理完成後還需要加到最終生成骨架屏節點的 style 上,這樣骨架屏文件可能比原來完整的頁面結構還大,這肯定不是我們希望的。
優化後的方案是用 getBoundingClientRect 和 getComputedStyle 獲取元素相關屬性,然後直接通過絕對定位的方式來生成最終的骨架屏節點。這樣在頁面上最終需要的屬性主要是 position、z-index、top、left、width、height、background、border-radius。除了無法保證頁面原始的 DOM 結構,其它需求基本都可以滿足,也更加聚焦於節點的處理。
主要實現流程如下圖:
該方案目前主要應用於馬蜂窩電商業務的多頁面項目中,包括下單頁、簽證頁等,以下單頁為例,展示效果如下圖:
2. 實現方式
- 生成骨架屏
(1) config.js 配置
(2)Puppeteer 新打開頁面並返回瀏覽器實例、openPage
(3)在瀏覽器環境里執行 evalDom.js 和 evalDom.js 中處理 node 節點的主要邏輯
-
上述 rootNode 為根節點,默認為 document.body 或者可以由開發指定
-
主要邏輯為判斷當前節點是否需要忽略、是否設置了背景圖片、是否含有文本資訊、開發是否指定了當前節點的處理方式等,對滿足條件的渲染其對應的骨架屏節點,否則處理當前節點的子節點
-
所有節點處理完成後,調用 showBlocks 將生成的骨架屏節點拼接位 HTML 字元串,以便後續處理
(4) getRenderStyle 生成骨架屏樣式
const styles = [ 'position: fixed', `z-index: ${zIndex}`, `top: ${top}%`, `left: ${left}%`, `width: ${width}%`, `height: ${height}%`, 'background: '+(background || '#eee'), ]; const radius = getStyle(node, 'border-radius'); radius && radius != '0px' && styles.push(`border-radius: ${radius}`); blocks.push(`<div style="${styles.join(';')}"></div>`);
- zIndex、top、left、width、height 為處理後的屬性,然後把所有骨架屏節點的字元串都 push 進 blocks 這個數組中。
(5) 最終生成骨架屏的 HTML 文件如下:
- 插入骨架屏
在項目入口 index.html 文件內添加
<body> <div id="app"> </div> <% if(htmlWebpackPlugin.options.hasSkeleton) { %> <div id="skeleton"><!-- 骨架屏通過htmlWebpackPlugin在啟動打包的時候自動注入 --> <%= htmlWebpackPlugin.options.loading.html %> </div> <% } %> <!-- built files will be auto injected --> </body>
四、 總結
目前,該方案已經支援由開發主動控制骨架屏生成時間,這樣就避免了頁面重定向的過程中無法生成正確的骨架屏,同時可以支援在本地開發時生成骨架屏。未來我們將實現支援開發自定義生成骨架屏節點的樣式和組件骨架屏的生成,並優化 evalDom.js 內部節點過濾、處理的演算法。敬請期待!
最後,我們正在招聘資深前端開發工程師,歡迎感興趣的同學發送簡歷至:[email protected]。
本文作者:康岑波、孫昊男,馬蜂窩電商平台前端研發工程師。