最近在寫錯誤上報,記錄一下,若是對你有所幫助,榮幸之至;第一次寫,有點囉嗦,見諒!
大概分爲三個部分:javascript
1、錯誤收集
js的錯誤通常分爲:運行時錯誤、資源加載錯誤、網絡請求錯誤;
對於語法錯誤、資源加載錯誤,供咱們選擇的錯誤收集方式通常是:css
window.addEventListener('error', e => {}, true); window.onerror = function (msg, url, line, col, error) {}
**劃重點:**
html
所以咱們能夠這樣收集錯誤:java
window.addEventListener("error", e => { if (!e) { return; } console.log(e.message);//錯誤信息 conosle.log(e.filename);//發生錯誤的文件名 console.log(e.lineno);//發生錯誤的行號(代碼壓縮合並對值有影響) console.log(e.colno);//發生錯誤的列號(代碼壓縮合並對值有影響) const _target = e.target || e.srcElement; if (!_target) { return; } if (_target === window) { //語法錯誤 let _error = e.error; if (_error) { console.log(_error.stack);//錯誤的堆棧信息 } } else { // 元素錯誤,好比引用資源報錯 let _src = _target.src; console.log(_src);//_src: 錯誤的資源路徑 } }, true);
當是運行時的語法錯誤時,咱們能夠拿到報錯的行號,列號,錯誤信息,錯誤堆棧,以及發生錯誤的腳本的路徑及名字。
當是資源路徑錯誤時,咱們能夠拿到錯誤資源的路徑及名字。jquery
至此,咱們就拿到了想要的資源加載錯誤、運行時語法錯誤的信息,那ajax網絡請求錯誤怎麼辦呢?ajax
此時:有兩個方式能夠選擇後端
throw new Error('拋出的一個錯誤'); console.error('打印一個錯誤');//下面會講
咱們前面定義的方法能夠收集到throw new Error
拋出的錯誤,可是要注意,拋出錯誤一樣也會阻斷後續的程序,使用的時候要當心;若是你的項目中也封裝了http請求的話,可參照下面代碼:跨域
//基於jquery function ajaxFun (params) { var _d = { type: params.type || 'POST', url: params.url || '', data: params.data || null, dataType: params.dataType || 'JSON', contentType: params.contentType || 'application/x-www-form-urlencoded', beforeSend: function (request) { }, success: function (data, status, xhr) { }, error: function (xhr, type, error) { throw new Error(params.url + '請求失敗'); } } $.ajax(_d); }
上面的代碼是用jquery
封裝的請求,我在error
方法裏面拋出了這個ajax請求的錯誤,由於拋出錯誤後面沒有其餘業務邏輯,不會有什麼問題,這裏我只要求收集ajax的error
方法錯誤,若是你的項目要求處理全部異常錯誤,好比token失效致使的登錄失敗,就須要在success函數裏面也作處理了。可是,要注意throw new Error('拋出的一個錯誤')
與console.error('打印一個錯誤')
的區別。數組
當使用console.error打印錯誤時,前面的window.addEventListener
方式無法收集到,可是咱們能夠經過其餘方式收集到錯誤,下面是一個更特殊的例子;promise
**特例:**
js運用範圍很廣,有些狀況,這樣是不可以收集到咱們想要的錯誤的;
打個比方,咱們用 cocos creator
引擎寫遊戲時,加載資源是使用引擎的方法,當發生資源不存在的錯誤時,咱們是不知道的,可是,咱們發現 cocos creator
引擎會將錯誤打印到控制檯,那也是引擎作的操做,咱們一番順藤摸瓜,會發現,cocos creator
引擎在底層報錯都是用cc.error
,翻看cc.error
的源碼,咱們就看見了咱們想看見的東西了console.error()
,這樣一來,知道錯誤是怎麼來的,就好辦了。(具體狀況,具體對待,這裏只是恰巧cocos是這麼處理的,其餘引擎可能不太同樣)
let _windowError = window.console.error; window.console.error = function () { let _str = JSON.stringify(arguments); console.log(_str); _windowError && _windowError.apply(window, arguments); }
複寫console.error
後,不管和人在何處使用這個函數,咱們均可以保證這個打印被咱們處理過,
記住,必定要先將原來的console.error
接收一下,而且在實現咱們須要的業務後,執行原來console.error
,
保證不會影響到其餘的邏輯。
2、錯誤篩選
也許你會疑惑?不是全部的錯誤都上報麼,爲何要篩選呢?
大多數狀況,咱們收集到錯誤,而後上報便可,
可是,有時候,會有循環報錯
、資源加載失敗一直重試,一直失敗
等種種特殊狀況,若是按照正常的上報流程,那麼可能會發生在短短几秒的時間內,收集到了上千、上萬條數據,致使程序卡頓,甚至是崩潰。
所以,咱們須要對錯誤進行篩選。
let _errorMap = {};//用於錯誤篩選的對象; let _errorArg = [];//存放錯誤信息的數組;
全局維護一個_errorMap,用於錯誤篩選的對象,每當有錯誤時,咱們按照約定好的規則,組成一個key,和_errorMap已經存在的key進行比對,若是不存在,證實是新的錯誤,須要上報,若是是已經上報的錯誤,就再也不處理。
固然,爲了防止_errorMap無限大、以及錯誤漏報,當_errorMap的key的數量大於必定數量時,咱們須要將_errorMap的key清空,這時候可能出現前面已經上報的錯誤再次上報,可是沒關係,這個重複能夠接受。
這個臨界值能夠根據實際狀況定,我項目中最大值爲100。
對於上面這個約定好的規則,其實就是根據咱們上面收集到的有關錯誤的信息,組成的一個惟一key值,能實現惟一性且越短越好便可
//上面的代碼,複製下來,方便看 window.addEventListener("error", e => { if (!e) { return; } console.log(e.message);//錯誤信息 conosle.log(e.filename);//發生錯誤的文件名 console.log(e.lineno);//發生錯誤的行號(代碼壓縮合並對值有影響) console.log(e.colno);//發生錯誤的列號(代碼壓縮合並對值有影響) const _target = e.target || e.srcElement; if (!_target) { return; } if (_target === window) { //語法錯誤 let _error = e.error; if (_error) { console.log(_error.stack);//錯誤的堆棧信息 } } else { // 元素錯誤,好比引用資源報錯 let _src = _target.src; console.log(_src);//_src: 錯誤的資源路徑 } }, true);
對於語法錯誤,能夠根據報錯的文件名,行號,列號,組成key let _key = `${e.filename}_${e.lineno}_${e.colno}`; 對於資源加載錯誤,能夠根據錯誤資源的路徑做爲key: let _key = e.src;
拿到key以後,咱們就能夠存貯錯誤了,
下面是存儲的完整代碼:
function _sendErr(key, errType, errMsg) { //篩選 if (_ErrorMap.hasOwnProperty(key)) { //篩選到相同的錯誤,可將值加一,能夠判斷錯誤出現的次數 _ErrorMap[key] += 1; return; } //閾值 if (_ErrorArg.length >= 100) { return; } //存儲錯誤 //對於要發給後端的數據,可根據需求組織,數據結構 _ErrorArg.push({ errType: errType,//錯誤類型 errMsg: errMsg || '',//錯誤信息 ver: _ver || '',//版本號 timestamp: new Date().getTime(),//時間戳 }); //存放錯誤信息的數組的閾值 if (Object.keys(_ErrorMap).length >= 100) { //達到閾值以後,清空去重對象 _ErrorMap = {}; } _ErrorMap[key] = 1; }
存儲錯誤的數組也須要閾值,實際運用中,咱們能夠控制每次上報的錯誤條數,可是,必定得記得已經上報的錯誤必定要從數組中移出。此外,上報的數據結構根據需求能夠調整,通常包含錯誤信息、堆棧信息、加載失敗資源的路徑。
3、錯誤上報
難道不是一收集到錯誤就上報?
同時出現一個兩個錯誤,固然能夠當即上報,
可是若是千百個錯誤在短短的幾秒鐘出現,就會出現網絡擁堵,甚至是程序崩潰。
所以,通常都會全局維護一個計時器,延遲上報;
let _ErrorTimer = null; timerError(); function timerError() { clearTimeout(_ErrorTimer); let _ErrorArg = g.IndexGlobal.ErrorArg;//前面提到的全局錯誤存貯數組 let _ErrorArgLength = _ErrorArg.length; if (_ErrorArgLength > 0) { let _data = [];//要發送的錯誤信息,由於是一次性發5條,放零時數組中。 //組織要發送的錯誤信息 for (let i = 0; i < _ErrorArgLength; i++) { if (_data.length >= 5) { break; } _data.push(_ErrorArg.shift()); } if (_data.length) { //發送錯誤信息 //jq ajax g.IndexGlobal.errorSend(_data, function (p) { //失敗 //若是發送失敗,將未發送的數據,從新放入存儲錯誤信息的數組中 if (p && p.data && p.data.data) { if (_ErrorArg.length >= 100) { return; } let _ag = p.data.data; try { g.IndexGlobal.ErrorArg.push(...JSON.parse(_ag)); } catch (error) { } } }); } } //計時器間隔,當數組長度大於20時,一秒執行一次,默認2秒一次 let _ti = _ErrorArgLength >= 20 ? 1000 : 2000; _ErrorTimer = setTimeout(timerError, _ti); }
咱們能夠根據錯誤的數量,調整錯誤上報的頻率。可是這個間隔通常不要過小,否則容易出問題。
4、注意事項
1.不管是window.addEventLister
仍是console.error
,在咱們定義這些方法以前報的全部錯誤,咱們是收集不到的,
怎麼處理呢,很簡單,js順序執行,咱們能夠將相關代碼放在最前頭,
<!DOCTYPE html> <html> <script> //處理錯誤的代碼 window.addEventLister; console.error = function(){} </script> <head> <meta charset="utf-8"> <link rel="stylesheet" href=""> <link rel="stylesheet" href=""> </head> <body> </body> <script src="js/zepto.min.js"></script> <script src="js/a.js"></script> <script> //開始錯誤上報計數器 </script> </html>
可是,要注意,放在最前面的是處理錯誤的邏輯,上報的計時器不能當即開啓,由於,此時jquery 還沒加載,
計時器開啓放在至少jquery加載完成以後。
2.必定要作好處理錯誤部分代碼的容錯處理,否則業務邏輯代碼還沒報錯,處理錯誤的部分反而報錯就很差了。
3.當你直接雙擊html,在瀏覽器打開時,錯誤收集機制可能不會正確工做,例如沒有行號,列號,文件名,錯誤信息僅僅是Script Error
,這是由於onerror MDN
當加載自 不一樣域的腳本中發生語法錯誤時,爲避免信息泄露(參見 bug 363897),語法錯誤的細節將不會報告,而代之簡單的**"Script error."**
。在某些瀏覽器中,經過在<script>
使用`[crossorigin](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script#attr-crossorigin)`
屬性並要求服務器發送適當的 CORS HTTP 響應頭,該行爲可被覆蓋。一個變通方案是單獨處理"Script error.",告知錯誤詳情僅能經過瀏覽器控制檯查看,沒法經過JavaScript訪問。
處理方式爲:服務端添加Access-Control-Allow-Origin
,頁面在script
標籤中配置 crossorigin="anonymous"
。這樣,便解決了由於跨域而帶來的問題。
5、完整代碼
<!DOCTYPE html> <html> <script> //處理錯誤的命名空間 window['errorSpace'] = { ErrorTimer: null, //全局錯誤上報計時器 ErrorArg: [], //全局錯誤存儲數組 ErrorMap: {}, //用於錯誤篩選的對象 //存儲錯誤信息 PushError: function (key, errMsg) { let _ErrorMap = window.errorSpace.ErrorMap; let _ErrorArg = window.errorSpace.ErrorArg; //篩選 if (_ErrorMap.hasOwnProperty(key)) { //篩選到相同的錯誤,可將值加一,能夠判斷錯誤出現的次數 _ErrorMap[key] += 1; return; } //閾值 if (_ErrorArg.length >= 100) { return; } //存儲錯誤 //對於要發給後端的數據,可根據需求組織,數據結構 _ErrorArg.push({ errMsg: errMsg || '', //錯誤信息 ver: '', //版本號 timestamp: new Date().getTime(), //時間戳 }); //存放錯誤信息的數組的閾值 if (Object.keys(_ErrorMap).length >= 100) { //達到閾值以後,清空去重對象 _ErrorMap = {}; } _ErrorMap[key] = 1; }, //錯誤上報函數 ErrorSend: function () { clearTimeout(window.errorSpace.ErrorTimer); let _ErrorArg = window.errorSpace.ErrorArg; //前面提到的全局錯誤存貯數組 let _ErrorArgLength = _ErrorArg.length; if (_ErrorArgLength > 0) { let _data = []; //要發送的錯誤信息,由於是一次性發5條,放零時數組中。 //組織要發送的錯誤信息 for (let i = 0; i < _ErrorArgLength; i++) { if (_data.length >= 5) { break; } _data.push(_ErrorArg.shift()); } if (_data.length) { //發送錯誤信息 //jq ajax var _d = { type: 'POST', url: '', data: _data || null, dataType: 'JSON', contentType: 'application/x-www-form-urlencoded', success: function (data, status, xhr) { //上報失敗,將錯誤從新存儲 //這是假設服務端返回的數據結構是{status: 200} if (data.status !== 200) { //失敗 try { //直接存入 //此處沒有對_ErrorArg的長度進行判斷,因此會溢出一次,使得錯誤錯誤儘量的保留,問題不大,也能夠不讓溢出 _ErrorArg.push(..._data); } catch (error) { console.log(error); } } }, error: function (xhr, type, error) { //上報失敗,將錯誤從新存儲 try { //直接存入 //此處沒有對_ErrorArg的長度進行判斷,因此會溢出一次,使得錯誤錯誤儘量的保留,問題不大,也能夠不讓溢出 _ErrorArg.push(..._data); } catch (error) { console.log(error); } } } $.ajax(_d); } } //計時器間隔,當數組長度大於20時,一秒執行一次,默認2秒一次 let _ti = _ErrorArgLength >= 20 ? 1000 : 2000; window.errorSpace.ErrorTimer = setTimeout(window.errorSpace.ErrorSend, _ti); }, }; //錯誤收集 window.addEventListener("error", e => { if (!e) { return; } let _err_msg = ''; //要上報的錯誤信息 let _r = 0; //發生錯誤的行號 let _l = 0; //發生錯誤的列號 let _fileName = ''; //發生錯誤的文件名 const srcElement = e.target || e.srcElement; if (!srcElement) { return; } if (srcElement === window) { //語法錯誤 let _error = e.error; if (_error) { _err_msg = _error.message + _error.stack; _r = e.lineno || 0; _l = e.colno || 0; _fileName = e.filename || ''; } } else { // 元素錯誤,好比引用資源報錯 if (srcElement.src) { _err_msg = srcElement.src; _fileName = srcElement.src; } } let _key = `${_fileName}_${_r}_${_l}`; window.errorSpace.PushError(_key, _err_msg); }, true); //處理console.error; let _windowError = window.console.error; window.console.error = function () { let _str = JSON.stringify(arguments); window.errorSpace.PushError(_str, _str); _windowError && _windowError.apply(window, arguments); } </script> <head> <meta charset="utf-8"> <link rel="stylesheet" href=""> <link rel="stylesheet" href=""> </head> <body> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> //開始錯誤上報計數器 window.errorSpace && window.errorSpace.ErrorSend && window.errorSpace.ErrorSend(); </script> </html>