<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                &emsp;&emsp;頁面奔潰包含兩種場景,第一種是瀏覽器在加載網頁時遇到問題導致的奔潰,另一種是因為腳本渲染出錯導致頁面空白無內容的奔潰。 &emsp;&emsp;前段時間運營抱怨有張活動頁出現了空白(第二種奔潰場景),導致用戶無法訪問,希望我們能主動監控到這種情況,而不是通過用戶的上報。 &emsp;&emsp;后面和運維溝通,他那邊目前只能監控接口的訪問情況,無法監控靜態資源,若要監控得自己想辦法實現。 &emsp;&emsp;首先想到的自然是利用現有的監控系統來了解頁面空白情況,例如某個項目5分鐘內沒有監控日志,那就認為出現了頁面奔潰。 &emsp;&emsp;急匆匆的寫了段定時任務,放到線上運行,發現這樣監控會有一個很大漏洞。因為某些項目的訪問量本來就不高,5分鐘內沒有日志是屬于正常情況,所以只得作罷。 &emsp;&emsp;2023-01-16 經過 TypeScript 整理重寫后,正式將監控系統的腳本開源,命名為?[shin-monitor](https://github.com/pwstrick/shin-monitor)。 ## 一、頁面奔潰 &emsp;&emsp;首先來解決第一種奔潰場景,在網上搜了些關鍵字,發現了些有用的資料,例如[如何監控網頁崩潰](https://zhuanlan.zhihu.com/p/40273861),[前端崩潰監控優化歷程](https://www.jackpu.com/web-qian-duan-crash-jian-kong-you-hua-li-cheng/)等。 &emsp;&emsp;這些資料提供了一個全新的思路來監控頁面奔潰,基于Service Worker的崩潰統計方案。 &emsp;&emsp;簡單地說就是一種心跳檢測機制,在頁面的腳本中創建Service Worker工作線程,然后定時地向該線程發送消息,即使網頁奔潰了,線程還能存活。 &emsp;&emsp;在線程中接收消息并比對時間,當間隔時間大于15秒時,就認為超時沒有心跳了,頁面處于奔潰階段,向監控系統上報相關信息。 &emsp;&emsp;在我操刀實現的時候,Service Worker沒有運行成功,后面就改成了[Web Worker](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API)。 &emsp;&emsp;工作線程的代碼保存在sw.js(如下所示),在參考一篇[Web Workers](https://www.html5rocks.com/zh/tutorials/workers/basics/%20)的文章時,他提到在線程中可以navigator對象,該對象正好有個sendBeacon()方法,可用于跨域請求。 &emsp;&emsp;但是沒想到線程中用的[WorkerNavigator](https://developer.mozilla.org/en-US/docs/Web/API/WorkerNavigator),并沒有該方法,后面無奈改成了[fetch()](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch)。 &emsp;&emsp;但是有跨域問題,要么在響應時加上跨域頭,要么就無視直接發送,因為瀏覽器只會攔截響應不會攔截請求。 ~~~ var CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 檢查一次 var CRASH_THRESHOLD = 15 * 1000; // 15s 超過15s沒有心跳則認為已經 crash var pages = {}, timer; function send(param) { fetch(param.src); }; function checkCrash() { var now = Date.now() for (var id in pages) { var page = pages[id]; if ((now - page.t) > CRASH_THRESHOLD) { // 上報 crash delete pages[id]; send(page.data); } } if (Object.keys(pages).length == 0) { clearInterval(timer) timer = null } } self.addEventListener('message', (e) => { var data = e.data; if (data.type === 'heartbeat') { // console.log('heartbeat'); pages[data.id] = { t: Date.now(), data: data.data } if (!timer) { timer = setInterval(function () { checkCrash() }, CHECK_CRASH_INTERVAL) } } else if (data.type === 'unload') { delete pages[data.id] } }) ~~~ &emsp;&emsp;在網頁中加的代碼如下,由于Worker加載的腳本有同源策略的限制,所以腳本和頁面需要在相同的域名中。 ~~~ function monitorCrash(param) { var isCrash = param.isCrash; if (!isCrash || !window.Worker) return; var worker = new Worker("/sw.js"); var HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒發一次心跳 var sessionId = getIdentity(); var heartbeat = function () { worker.postMessage({ type: "heartbeat", id: sessionId, data: { //在頁面奔潰時,上報數據,需要將上報地址一起傳遞 src: param.src } }); }; window.addEventListener("beforeunload", function () { worker.postMessage({ type: "unload", id: sessionId }); }); var timer = setInterval(heartbeat, HEARTBEAT_INTERVAL); heartbeat(); } ~~~ &emsp;&emsp;上線后先在管理后臺做測試,管理后臺使用的是PC瀏覽器,馬上就發現了比較嚴重的誤報問題。 &emsp;&emsp;分析下來有可能是網頁在標簽欄不活動的時候,影響了定時器的執行,再次活動計算兩個時間段的間隔,很有可能超出了15秒,而上報奔潰日志。 &emsp;&emsp;鑒于此,在沒有完美解決方案之前,暫時將此功能下架。 ## 二、頁面空白 &emsp;&emsp;再來解決第二種奔潰場景,現在開發都會依托React或Vue等庫或框架,而這些都是用腳本來渲染出DOM結構的。 &emsp;&emsp;一旦在渲染時出現腳本錯誤(例如未定義的變量、瀏覽器不支持的語法等)就會中斷渲染,從而就會出現頁面無內容的情況。 &emsp;&emsp;這類監控并不需要使用Web Worker,只要我的監控SDK在業務腳本之前引入,就能保證監控代碼正常運行。 **1)自定義白屏方法** &emsp;&emsp;監控原理就是加個定時器,查看渲染容器中是否是空白,若是空白就上報并關閉定時器,否則循環監控。 &emsp;&emsp;例如后臺管理系統采用的是React,在HTML中會聲明一個div元素,內容都會渲染到該元素中。 ~~~html <div id="root"></div> ~~~ &emsp;&emsp;自定義一個關鍵DOM的判斷條件,如下所示,在定時器中循環執行。 ~~~ shin.setParam({ validateCrash: () => { //當root標簽中的內容為空時,可認為頁面已奔潰 return { success: document.getElementById("root").innerHTML.length > 0, prompt: "頁面出現空白" }; } }); ~~~ &emsp;&emsp;此處還有個小坑,就是定時器的運行時機,不能太早,太早判斷的話,div元素中肯定沒有內容,后面就將判斷時機移到了[DOMContentLoaded](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/DOMContentLoaded_event)事件中。 &emsp;&emsp;下面是監控白屏的主要邏輯,isCrash 是一個監控開關,document.body.clientHeight 是指內容高度與上下內邊距的和。 &emsp;&emsp;在我們這邊的頁面中, body 都不會有內邊距,所以該判斷適用,當然,具體可根據業務場景做自定義的兜底處理。 &emsp;&emsp;2022-12-26 注意,若自定義了 validateCrash() 方法,那么就不能走默認的白屏判斷條件了。 ~~~ function monitorCrash(param) { var isCrash = param.isCrash; var validateCrash = param.validateCrash; if (!isCrash || !window.Worker) { return; } var HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒發一次心跳 var crashHeartbeat = function () { // 是否自定義了規則 if (validateCrash) { var result = validateCrash(); // 符合自定義的奔潰規則 if (result && !result.success) { handleError({ type: ERROR_CRASH, desc: { prompt: result.prompt, url: location.href } }); // 關閉定時器 clearInterval(timer); // worker = null; } } else if (_isWhiteScreen()) { // 兜底白屏算法,可根據自身業務定義 // 查詢第一個div var currentDiv = document.querySelector("div"); // 增加 html 字段是為了驗證是否出現了誤報 handleError({ type: ERROR_CRASH, desc: { prompt: "頁面沒有高度", url: location.href, html: currentDiv ? currentDiv.innerHTML : "" } }); clearInterval(timer); } }; var timer = setInterval(crashHeartbeat, HEARTBEAT_INTERVAL); crashHeartbeat(); // 立即執行一次 // 5分鐘后自動取消定時器 setTimeout(function () { // 關閉定時器 clearInterval(timer); }, 1000 * 300); } ~~~ **2)\_isWhiteScreen()** &emsp;&emsp;2022-12-26 \_isWhiteScreen() 是一個兜底的白屏算法,可根據自身業務定義。 &emsp;&emsp;最初的判斷條件是 document.body.clientHeight 是否大于 0,但是如果 body 的所有子元素都是絕對定位時,那么它的高度同樣也會變成 0。 &emsp;&emsp;由此就給出了優化后的白屏算法,判斷 body 元素的子元素的高度是否都是 0,若都是 0,那么就是白屏。 ~~~ function _isWhiteScreen() { // 羅列 body 的子元素 var children = [].slice.call(document.body.children); // 過濾出高度不為 0 的子元素 var visibles = children.filter(function (element) { return element.clientHeight > 0; }); return visibles.length === 0; } ~~~ &emsp;&emsp;但是上線后,出現了大量的誤報,分析網頁代碼后,發現頁面有個比較差的交互,那就是在進入時會有極短的時間白屏,在等待從客戶端中拿用戶信息。 &emsp;&emsp;兩個方案,第一個是在那段時間增加 loading 特效,滿足判斷條件;第二個是為白屏監控增加延遲時間,例如延遲 1 秒后再判斷是否真的白屏。 &emsp;&emsp;注意,現在的頁面以 CSR(客戶端渲染)為主,預留一個空 div 元素在頁面中。大部分情況下,只有在拿到接口數據后,才會對頁面進行渲染。 &emsp;&emsp;如果這個接口通信持續了一秒以上,那么就會觸發白屏檢測,此時就會上報為白屏。雖然這是個誤報,但是這么重要的接口居然超過 1 秒,那還是有必要優化的。 &emsp;&emsp;2024-10-09 正巧發現一個接口返回比較慢,分析后發現是因為響應內容比較大(2M),遇到網絡比較差的時候,通信時間就會拉長,原來里面有張圖被內嵌為 base64,只需將其改成 url 訪問即可。 ~~~ setTimeout(function () { monitorCrash(shin.param); }, 1000); ~~~ &emsp;&emsp;在翻看白屏記錄時,又發現了 \_isWhiteScreen() 函數的漏洞。 &emsp;&emsp;那就是如果 body 只有一個子元素,但是子元素中的元素恰好都是絕對定位,那么此時就會誤判,body 子元素的高度確實是 0。 &emsp;&emsp;再度優化后,會對 body 的子元素做深度優先搜索,若已找到一個有高度的元素、或若元素隱藏、或元素有高度并且不是 body 元素,則結束搜索。 &emsp;&emsp;2022-12-29 將 node.clientHeihgt 改成 node.getBoundingClientRect().height,前者會將內容高度和上下內邊距相加,后者還會加上邊框。 &emsp;&emsp;但是[clientHeihgt](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight)不會計算行內元素(例如 span、a 等)的高度,[getBoundingClientRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect)會計算。 &emsp;&emsp;并且 getBoundingClientRect() 還會將諸如[transform: scale(0.5)](https://stackoverflow.com/questions/32438642/clientwidth-and-clientheight-report-zero-while-getboundingclientrect-is-correct)變換元素尺寸后,得到最終計算后的值。 ~~~ function _isWhiteScreen() { var visibles = []; var nodes = []; //遍歷到的節點的關鍵信息,用于查明白屏原因 // 深度優先遍歷子元素 var dfs = (node) => { var tagName = node.tagName.toLowerCase(); var rect = node.getBoundingClientRect(); // 選取節點的屬性作記錄 var attrs = { id: node.id, tag: tagName, className: node.className, display: node.style.display, height: rect.height }; if (node.src) { attrs.src = node.src; // 記錄圖像的地址 } if (node.href) { attrs.href = node.href; // 記錄鏈接的地址 } nodes.push(attrs); // 若已找到一個有高度的元素,則結束搜索 if (visibles.length > 0) return; // 若元素隱藏,則結束搜索 if (node.style.display === "none") return; // 若元素有高度并且不是 body 元素,則結束搜索 if (rect.height > 0 && tagName !== "body") { visibles.push(node); return; } node.children && [].slice.call(node.children).forEach((child) => { var tagName = child.tagName.toLowerCase(); // 過濾腳本和樣式元素 if (tagName === "script" || tagName === "link") return; dfs(child); }); }; dfs(document.body); return { visibles: visibles, nodes: nodes }; } ~~~ &emsp;&emsp;2022-12-28 最近遇到一個白屏誤報的問題,翻看了好幾遍代碼,也沒看出有什么問題,于是將遍歷的節點的關鍵信息,也一并上報,幫助排查。 &emsp;&emsp;通過這些關鍵信息,可以識別出節點在 HTML 結構中所處的位置。 &emsp;&emsp;2022-12-29 今天終于破解了昨日百思不得其解的問題,雖然得到的所有子元素的高度都為 0,但是回放又能看到元素內容。 &emsp;&emsp;我一度懷疑是白屏判斷的觸發時機問題,特地記錄的時間戳,但的確是在指定時間運行。通過查看記錄的 UA 信息,可以判斷是在 PC 的瀏覽器中上報的。 &emsp;&emsp;進一步縮小范圍可知,和一個 iframe 中的網頁有關,當包含 iframe 的彈框關閉時,彈框會被隱藏(display:none)。 &emsp;&emsp;由于有一個定時器在輪詢判斷是否白屏,此時,在 iframe 內,因為被隱藏的緣故,因此所有的元素高度都將是 0。 &emsp;&emsp;這種情況比較特殊,目前的做法是將彈框關閉時,其內容直接銷毀而不再是隱藏。 &emsp;&emsp;注意,在 monitorCrash() 函數中,需要對 else 分支內的 \_isWhiteScreen() 做相應的處理。 ~~~ // 兜底白屏算法,可根據自身業務定義 var whiteObj = _isWhiteScreen(); if (whiteObj.visibles.length > 0) { return; } // 查詢第一個div var currentDiv = document.querySelector("div"); // 增加 html 字段是為了驗證是否出現了誤報 handleError({ type: ERROR_CRASH, desc: { prompt: "頁面沒有高度", url: location.href, html: currentDiv ? currentDiv.innerHTML : "", timestamp: _calcCurrentTime(), fontSize: document.documentElement.style.fontSize, // 根節點的字體大小 nodes: whiteObj.nodes } }); clearInterval(timer); ~~~ &emsp;&emsp;這個算法還有優化的空間,假如碰到一種極端情況,body 只有一個 div 子元素,沒有內容,但是聲明了高度或內邊距,那么就會認為當前不是白屏。 &emsp;&emsp;不過目前,公司的頁面開發暫時不會涉及此類情況,所以先不考慮了。應該還有很多其他的極端情況,待到搜集到上報,再一并做優化。 **3)isCrash** &emsp;&emsp;2022-12-07 一開始 isCrash 默認標記為 false,也就是關閉監控的,后面默認打開后,線上出現白屏的頁面一下子增加了四五百左右。 &emsp;&emsp;接下來就是驗證上報的白屏是否準確,下面是上報的一條記錄,它有一串字符身份信息,例如 syqgpsyz4s。 ~~~ { "type": "crash", "desc": { "prompt": "頁面沒有高度", "url": "https://www.xxx.com/chat.html?matchId=100", "html": "" } } ~~~ &emsp;&emsp;根據身份信息,再去日志明細中查找他的前后動作,發現只有一條記錄,也就是既沒有腳本錯誤,也沒有接口請求。 :-: ![](https://img.kancloud.cn/15/93/15936c35499d176010988afa6deb3096_2968x1456.png =800x) &emsp;&emsp;再根據此身份去查詢性能監控的記錄 ID,找出當時靜態資源的瀑布圖,在此圖中,并沒有發現資源異常。 :-: ![](https://img.kancloud.cn/d4/50/d4504aa4153751f52f81c4bb242b2a33_1946x1190.png =800x) &emsp;&emsp;但是當我直接請求 url 地址時,卻發現有 3 個資源的請求是 404,與正常頁面中的 3 個資源做比對,發現兩者的隨機后綴是不同的。 :-: ![](https://img.kancloud.cn/41/b5/41b588d0b22308328beef3d19ca566b3_1718x260.png =800x) &emsp;&emsp;現在恍然大悟,是 CDN 緩存刷新失敗導致的問題,問題馬上就定位到了。 &emsp;&emsp;還發現另一個問題是因為參數的值導致的白屏,首次使用下來,準確率還是蠻高的。 &emsp;&emsp;2022-12-19 還有一類不是 CDN 引起的資源報錯,那就是客戶端的緩存。客戶端會緩存 HTML 頁面,當訪問緩存頁面時,其中的資源必定已經不存在。 &emsp;&emsp;要破除緩存,就要給 URL 地址增加一個時間戳參數,好在客戶端中的活動頁面都是通過自研的[短鏈](https://www.cnblogs.com/strick/p/14299313.html)跳轉的,可以在短鏈映射真實地址時,自動增加時間戳參數。 &emsp;&emsp;關于資源瀑布圖,還有優化空間,可以將 404 資源標紅。同時也發現了靜態資源請求錯誤沒有記錄的問題。 &emsp;&emsp;去掉下面 if 語句中對 event.filename 的判斷,因為資源錯誤是沒有 filename 屬性的,這樣就能將此類資源錯誤記錄在案了。 ~~~ window.addEventListener( "error", function (event) { var errorTarget = event.target; // 過濾掉與業務無關的錯誤 if (event.message === "Script error." || !event.filename) { return; } if ( errorTarget !== window && errorTarget.nodeName && LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()] ) { handleError(formatLoadError(errorTarget)); } else { handleError( formatRuntimerError( event.message, event.filename, event.lineno, event.colno, event.error ) ); } }, true //捕獲 ); ~~~ &emsp;&emsp;2022-12-09 在優化白屏后的幾天,發現有誤報的情況發生,因為 html 屬性值中有內容。 &emsp;&emsp;打開這些頁面分析,發現有些內容的樣式是絕對定位或固定定位,也就是說這些內容并不會撐起 body 的高度。 &emsp;&emsp;那么要有高度,就需要等待其他元素渲染,若在上報白屏時,還沒渲染成功,那么就有可能誤報。 &emsp;&emsp;為了驗證自己的猜想,去查詢了下某條性能記錄的資源瀑布圖,發現在觸發 DOMContentLoaded 時,那些能撐起高度的資源還沒加載完成。 &emsp;&emsp;經測試發現,當因為腳本錯誤出現白屏時,兩個事件的觸發時機會很接近,而如果是正常情況,那么兩者會有些時間的間隔。 &emsp;&emsp;所以發生白屏時,也能減少因用戶快速關閉頁面而發生漏報的情況,因此最后決定將上報遷移到 load 事件中。 &emsp;&emsp;2022-12-13 在監控白屏時,發現有一類的白屏是由標簽欄切換引起的,因為在切換后會先將之前的列表清空,再去請求接口。 &emsp;&emsp;在等待數據時就會有那么一段白屏時間差,為了體驗好點,其實可以加一些過渡效果,例如加個 loading 等待。 ***** > 原文出處: [博客園-從零開始搞系列](https://www.cnblogs.com/strick/category/1928903.html) 已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看