前端白屏的檢測方案,讓你知道自己的頁面白了

語言: CN / TW / HK

theme: juejin highlight: androidstudio


前言

頁面白屏,絕對是讓前端開發者最為膽寒的事情,特別是隨著 SPA 專案的盛行,前端白屏的情況變得更為複雜且棘手起來( 這裡的白屏是指頁面一直處於白屏狀態 )

要是能檢測到頁面白屏就太棒了,開發者誰都不想成為最後一個知道自己頁面白的人😥

web-see 前端監控方案,提供了 取樣對比+白屏修正機制 的檢測方案,相容有骨架屏、無骨架屏這兩種情況,來解決開發者的白屏之憂

知道頁面白了,然後呢?

web-see 前端監控,會給每次頁面訪問生成一個唯一的uuid,當上報頁面白屏後,開發者可以根據白屏的uuid,去監控後臺查詢該id下對應的程式碼報錯、資源報錯等資訊,定位到具體的原始碼,幫助開發者快速解決白屏問題

白屏檢測方案的實現流程

取樣對比+白屏修正機制的主要流程:

1、頁面中間取17個取樣點(如下圖),利用 elementsFromPoint api 獲取該座標點下的 HTML 元素

2、定義屬於容器元素的集合,如 ['html', 'body', '#app', '#root']

3、判斷17這個取樣點是否在該容器集合中。說白了,就是判斷取樣點有沒有內容;如果沒有內容,該點的 dom 元素還是容器元素,若17個取樣點都沒有內容則算作白屏

4、若初次判斷是白屏,開啟輪詢檢測,來確保白屏檢測結果的正確性,直到頁面的正常渲染

取樣點分佈圖(藍色為取樣點):

point.png

如何使用

``` import webSee from 'web-see';

Vue.use(webSee, { dsn: 'http://localhost:8083/reportData', // 上報的地址 apikey: 'project1', // 專案唯一的id userId: '89757', // 使用者id silentWhiteScreen: true, // 開啟白屏檢測 skeletonProject: true, // 專案是否有骨架屏 whiteBoxElements: ['html', 'body', '#app', '#root'] // 白屏檢測的容器列表 }); ``` 下面聊一聊具體的分析與實現

白屏檢測的難點

1) 白屏原因的不確定

從問題推導現象雖然能成功,但從現象去推導問題卻走不通。白屏發生時,無法和具體某個報錯聯絡起來,也可能根本沒有報錯,比如關鍵資源還沒有載入完成

導致白屏的原因,大致分兩種:資源載入錯誤、程式碼執行錯誤

2) 前端渲染方式的多樣性

前端頁面渲染方式有多種,比如 客戶端渲染 CSR 、服務端渲染 SSR 、靜態頁面生成 SSG 等,每種模式各不相同,白屏發生的情況也不盡相同

很難用一種統一的標準去判斷頁面是否白了

技術方案調研

如何設計出一種,在準確性、通用型、易用性等方面均表現良好的檢測方案呢?

本文主要討論 SPA 專案的白屏檢測方案,包括有無骨架屏的兩種情況

方案一:檢測根節點是否渲染

原理很簡單,在當前主流 SPA 框架下,DOM 一般掛載在一個根節點之下(比如 <div id="app"></div> ),發生白屏後通常是根節點下所有 DOM 被解除安裝,該方法通過檢測根節點下是否掛載 DOM,若無則證明白屏

這是簡單明瞭且有效的方案,但缺點也很明顯:其一切建立在 白屏 === 根節點下 DOM 被解除安裝 成立的前提下,缺點是通用性較差,對於有骨架屏的情況束手無策

方案二:Mutation Observer 監聽 DOM 變化

通過此 API 監聽頁面 DOM 變化,並告訴我們每次變化的 DOM 是被增加還是刪除

但這個方案有幾個缺陷

1)白屏不一定是 DOM 被解除安裝,也有可能是壓根沒渲染,且正常情況也有可能大量 DOM 被解除安裝

2)遇到有骨架屏的專案,若頁面從始至終就沒變化,一直顯示骨架屏,這種情況 Mutation Observer 也束手無策

方案三:頁面截圖檢測

這種方式是基於原生圖片對比演算法處理白屏檢測的 web 實現

整體流程:對頁面進行截圖,將截圖與一張純白的圖片做對比,判斷兩者是否足夠相似

但這個方案有幾個缺陷:

1、方案較為複雜,效能不高;一方面需要藉助 canvas 實現前端截圖,同時需要藉助複雜的演算法對圖片進行對比

2、通用性較差,對於有骨架屏的專案,對比的樣張要由純白的圖片替換成骨架屏的截圖

方案四:取樣對比

該方法是對頁面取關鍵點,進行取樣對比,在準確性、易用性等方面均表現良好,也是最終採用的方案

對於有骨架屏的專案,通過對比前後獲取的 dom 元素是否一致,來判斷頁面是否變化(這塊後面專門講解)

取樣對比程式碼:

// 監聽頁面白屏 function whiteScreen() { // 頁面載入完畢 function onload(callback) { if (document.readyState === 'complete') { callback(); } else { window.addEventListener('load', callback); } } // 定義外層容器元素的集合 let containerElements = ['html', 'body', '#app', '#root']; // 容器元素個數 let emptyPoints = 0; // 選中dom的名稱 function getSelector(element) { if (element.id) { return "#" + element.id; } else if (element.className) {// div home => div.home return "." + element.className.split(' ').filter(item => !!item).join('.'); } else { return element.nodeName.toLowerCase(); } } // 是否為容器節點 function isContainer(element) { let selector = getSelector(element); if (containerElements.indexOf(selector) != -1) { emptyPoints++; } } onload(() => { // 頁面載入完畢初始化 for (let i = 1; i <= 9; i++) { let xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2); let yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10); isContainer(xElements[0]); // 中心點只計算一次 if (i != 5) { isContainer(yElements[0]); } } // 17個點都是容器節點算作白屏 if (emptyPoints == 17) { // 獲取白屏資訊 console.log({ status: 'error' }); } } }

白屏修正機制

若首次檢測頁面為白屏後,任務還沒有完成,特別是手機端的專案,有可能是使用者網路環境不好,關鍵的JS資源或介面請求還沒有返回,導致的頁面白屏

需要使用輪詢檢測,來確保白屏檢測結果的正確性,直到頁面的正常渲染,這就是白屏修正機制

白屏修正機制圖例:

point.png

輪詢程式碼:

// 取樣對比 function sampling() { let emptyPoints = 0; …… // 頁面正常渲染,停止輪詢 if (emptyPoints != 17) { if (window.whiteLoopTimer) { clearTimeout(window.whiteLoopTimer) window.whiteLoopTimer = null } } else { // 開啟輪詢 if (!window.whiteLoopTimer) { whiteLoop() } } // 通過輪詢不斷修改之前的檢測結果,直到頁面正常渲染 console.log({ status: emptyPoints == 17 ? 'error' : 'ok' }); } // 白屏輪詢 function whiteLoop() { window.whiteLoopTimer = setInterval(() => { sampling() }, 1000) }

骨架屏

對於有骨架屏的頁面,使用者開啟頁面後,先看到骨架屏,然後再顯示正常的頁面,來提升使用者體驗;但如果頁面從始至終都顯示骨架屏,也算是白屏的一種

骨架屏示例:

skeleton.gif

骨架屏的原理

無論 vue 還是 react,頁面內容都是掛載到根節點上。常見的骨架屏外掛,就是基於這種原理,在專案打包時將骨架屏的內容直接放到 html 檔案的根節點中

有骨架屏的html檔案:

html.png

骨架屏的白屏檢測

上面的白屏檢測方案對有骨架屏的專案失靈了,雖然頁面一直顯示骨架屏,但判斷結果頁面不是白屏,不符合我們的預期

需要通過外部傳參明確的告訴 SDK,該頁面是不是有骨架屏,如果有骨架屏,通過對比前後獲取的 dom 元素是否一致,來實現骨架屏的白屏檢測

完整程式碼:

``` /* * 檢測頁面是否白屏 * @param {function} callback - 回到函式獲取檢測結果 * @param {boolean} skeletonProject - 頁面是否有骨架屏 * @param {array} whiteBoxElements - 容器列表,預設值為['html', 'body', '#app', '#root'] / export function openWhiteScreen(callback, { skeletonProject, whiteBoxElements }) { let _whiteLoopNum = 0; let _skeletonInitList = []; // 儲存初次取樣點 let _skeletonNowList = []; // 儲存當前取樣點

// 專案有骨架屏 if (skeletonProject) { if (document.readyState != 'complete') { sampling(); } } else { // 頁面載入完畢 if (document.readyState === 'complete') { sampling(); } else { window.addEventListener('load', sampling); } } // 選中dom點的名稱 function getSelector(element) { if (element.id) { return '#' + element.id; } else if (element.className) { // div home => div.home return ('.' + element.className.split(' ').filter(item => !!item).join('.')); } else { return element.nodeName.toLowerCase(); } } // 判斷取樣點是否為容器節點 function isContainer(element) { let selector = getSelector(element); if (skeletonProject) { _whiteLoopNum ? _skeletonNowList.push(selector) : _skeletonInitList.push(selector); } return whiteBoxElements.indexOf(selector) != -1; } // 取樣對比 function sampling() { let emptyPoints = 0; for (let i = 1; i <= 9; i++) { let xElements = document.elementsFromPoint( (window.innerWidth * i) / 10, window.innerHeight / 2 ); let yElements = document.elementsFromPoint( window.innerWidth / 2, (window.innerHeight * i) / 10 ); if (isContainer(xElements[0])) emptyPoints++; // 中心點只計算一次 if (i != 5) { if (isContainer(yElements[0])) emptyPoints++; } } // 頁面正常渲染,停止輪訓 if (emptyPoints != 17) { if (skeletonProject) { // 第一次不比較 if (!_whiteLoopNum) return openWhiteLoop(); // 比較前後dom是否一致 if (_skeletonNowList.join() == _skeletonInitList.join()) return callback({ status: 'error' }); } if (window._loopTimer) { clearTimeout(window._loopTimer); window._loopTimer = null; } } else { // 開啟輪訓 if (!window._loopTimer) { openWhiteLoop(); } } // 17個點都是容器節點算作白屏 callback({ status: emptyPoints == 17 ? 'error' : 'ok', }); } // 開啟白屏輪訓 function openWhiteLoop() { if (window._loopTimer) return; window._loopTimer = setInterval(() => { if (skeletonProject) { _whiteLoopNum++; _skeletonNowList = []; } sampling(); }, 1000); } }

```

如果不通過外部傳參,SDK 能否自己判斷是否有骨架屏呢? 比如在頁面初始的時候,根據根節點上有沒有子節點來判斷

因為這套檢測方案需要相容 SSR 服務端渲染的專案,對於 SSR 專案來說,瀏覽器獲取 html 檔案的根節點上已經有了 dom 元素,所以最終採用外部傳參的方式來區分

總結

這套白屏檢測方案是從現象推導本質,可以覆蓋絕大多數 SPA 專案的應用場景

小夥們若有其他檢測方案,歡迎多多討論與交流 💕

這是前端監控的第三篇文章,前兩篇沒有看過的小夥伴也建議瞭解下

參考資料
如何實現前端白屏監控?
H5的白屏檢測方案實踐