做爲一個前端,在開發過程即使十分當心,自測充分,在不一樣用戶複雜的操做下也不免會出現程序員意想不到的問題,給公司或我的帶來巨大的損失。 這時一款可以及時上報錯誤和可以幫助程序員很好的解決錯誤的前端錯誤監控系統就必不可少了。 接下來咱們就聊聊常見的錯誤發生與處理。javascript
本文主要圍繞如下幾點討論:html
問題:前端
能夠閱讀監控類庫源碼 errorWatch 來加深理解,也能夠直接用於項目。java
例如,英文字符寫成中文字符。通常容易在開發時被發現。git
語法錯誤沒法被try
catch
處理程序員
try { const error = 'error'; // 圓角分號 } catch(e) { console.log('我感知不到錯誤'); } 複製代碼
JS引擎在執行腳本時,把任務分塊壓入事件棧,輪詢取出執行,每一個事件任務都有本身的上下文環境, 在當前上下文環境同步執行的代碼發生錯誤都能被try
catch
捕獲,保證後續的同步代碼被執行。github
try { error } catch(e) { console.log(e); } 複製代碼
常見的 setTimeout
等方法會建立新的事件任務插入事件棧中,待後續執行。 因此try
catch
沒法捕獲其餘上下文的代碼錯誤。web
try { setTimeout(() => { error // 異步錯誤 }) } catch(e) { console.log('我感知不到錯誤'); } 複製代碼
爲了便於分析發生的錯誤,通常利用 window.onerror
事件來監聽錯誤的發生。 它比try
catch
的捕獲錯誤信息的能力要強大。ajax
/** * @param {String} msg 錯誤描述 * @param {String} url 報錯文件 * @param {Number} row 行號 * @param {Number} col 列號 * @param {Object} error 錯誤Error對象 */ window.onerror = function (msg, url, row, col, error) { console.log('我知道錯誤了'); // return true; // 返回 true 的時候,異常不會向上拋出,控制檯不會輸出錯誤 }; 複製代碼
window.onerror
能夠捕獲常見語法、同步、異步錯誤等錯誤;window.onerror
沒法捕獲 Promise
錯誤、網絡錯誤;window.onerror
應該在全部JS腳本以前被執行,以避免遺漏;window.onerror
容易被覆蓋,在處理回調時應該考慮,被人也在使用該事件監聽。因爲網絡請求異常不會冒泡,應此須要在事件捕獲階段才能獲取到。 咱們能夠利用 window.addEventListener
。好比代碼、圖片等重要 CDN
資源掛了,能及時得到反饋是極爲重要的。chrome
window.addEventListener('error', (error) => { console.log('404 錯誤'); console.log(error); // return true; // 中斷事件傳播 }, true); 複製代碼
對於這類資源加載錯誤,在事件對象中能得到足夠的信息,配合短信、釘釘等第一時間通知開發者。
window.addEventListener('error', (e) => { if (e.target !== window) { // 避免重複上報 console.log({ url: window.location.href, // 引用資源地址 srcUrl: e.target.src, // 資源加載出錯地址 }) } }, true); 複製代碼
window.onerror
與 window.addEventListener
window.addEventListener
的好處,不怕回調被覆蓋,能夠監聽多個回調函數,但記得銷燬避免內存泄漏與錯誤。 但沒法獲取 window.onerror
那麼豐富的信息。通常只用window.addEventListener
來監控資源加載錯誤。
若是你在使用 promise
時未 catch
的話,那麼 onerror
也無能爲力了。
Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error'; }); 複製代碼
一樣你能夠利用 window.onunhandledrejection
或 window.addEventListener("unhandledrejection")
來監控錯誤。 接收一個PromiseError對象,能夠解析錯誤對象中的 reason
屬性,有點相似 stack
。
具體兼容處理在 TraceKit.js 能夠看到。
img
上報ajax
上報function report(errInfo) { new Image().src = 'http://your-api-website?data=' + errInfo; } 複製代碼
ajax
應使用的類庫而已,大同小異。
img
請求有長度限制,數據太大最好仍是用 ajax.post
。引用不一樣域名的腳本,若是沒有特殊處理,報錯誤了,通常瀏覽器處於安全考慮,不顯示具體錯誤而是 Script error
. 例如他人別有用心引用你的線上非開源業務代碼,你的腳本報錯信息固然不想讓他知道了。
若是解決自有腳本的跨域報錯問題?
CDN
的優點。HTTP response header
中設置 CORS
。Access-Control-Allow-Origin: You-allow-origin
;crossorigin
屬性,例如 <script src="http://www.xxx.com/index.js" crossorigin></script>
響應頭和crossorigin
取值問題
crossorigin="anonymous"
(默認),CORS
不等於 You-allow-origin
,不能帶 cookie
crossorigin="use-credentials"
且 Access-Control-Allow-Credentials: true
,CORS
不能設置爲 *
,能帶 cookie
。 若是 CORS
不等於 You-allow-origin
,瀏覽器不加載 js。當你對自由能掌握的資源作好了 cors
時,Script error
基本能夠過濾掉,不上報。
講了這麼多,還有一個很是重要的主題,如何分析我能捕獲的錯誤信息?
一個 JavaScript
錯誤一般由一下錯誤組成
開發者能夠經過不一樣方式來拋出一個JavaScript 錯誤:
推薦使用第二種,第三四種瀏覽器沒法就以上兩種方式生成追溯棧。
若是能解析每行追溯棧中的錯誤信息,行列在配合 SourceMap
不就能定位到每行具體源代碼了嗎。 問題在於不一樣瀏覽器在以上信息給出中,並無一個通用標準的格式。難點就在於解決兼容性問題。
例如 window.onerror
第五個參數 error 對象是2013年加入到 WHATWG
規範中的。 早期Safari 和 IE10尚未,Firefox是從14版本加入Error對象的,chrome 也是 2013 年才新加的。
window.onerror
是捕獲JS 錯誤最好的方法,當有一個合法的Error對象和追溯棧時才上報。 也能夠避免一些沒法干擾的錯誤,例如插件錯誤和跨域等一些信息不全的錯誤。
try catch
加強,拋出的錯誤信息較全,能夠彌補 window.onerror
的不足。但就像先前說過的, try catch
沒法捕獲異步錯誤和promise
錯誤,也不利用 V8
引擎性能優化。
例如騰訊的 BadJS,對如下推薦進行了try catch
包裹
具體是否須要作到如此細粒度的包裹,仍是視狀況而定。
例若有如下錯誤追溯棧(stack trace)
ReferenceError: thisIsAbug is not defined
at Object.makeError (http://localhost:7001/public/js/traceKit.min.js:1:9435)
at http://localhost:7001/public/demo.html:28:12
複製代碼
可以解析成一下格式
[ { "args" : [], "url" : "http://localhost:7001/public/js/traceKit.min.js", "func" : "Object.makeError", "line" : 1, "column" : 9435, "context" : null }, { "args" : [], "url" : "http://localhost:7001/public/demo.html", "func" : "?", "line" : 28, "column" : 12, "context" : null } ] 複製代碼
在有了行列和對應的 SourceMap
文件就能解析獲取源代碼信息了。
解析結果
處理代碼以下:
import { SourceMapConsumer } from 'source-map'; // 必須初始化 SourceMapConsumer.initialize({ 'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm', }); /** * 根據sourceMap文件解析源代碼 * @param {String} rawSourceMap sourceMap文件 * @param {Number} line 壓縮代碼報錯行 * @param {Number} column 壓縮代碼報錯列 * @param {Number} offset 設置返回臨近行數 * @returns {Promise<{context: string, originLine: number | null, source: string | null}>} * context:源碼錯誤行和上下附近的 offset 行,originLine:源碼報錯行,source:源碼文件名 */ export const sourceMapDeal = async (rawSourceMap, line, column, offset) => { // 經過sourceMap庫轉換爲sourceMapConsumer對象 const consumer = await new SourceMapConsumer(rawSourceMap); // 傳入要查找的行列數,查找到壓縮前的源文件及行列數 const sm = consumer.originalPositionFor({ line, // 壓縮後的行數 column, // 壓縮後的列數 }); // 壓縮前的全部源文件列表 const { sources } = consumer; // 根據查到的source,到源文件列表中查找索引位置 const smIndex = sources.indexOf(sm.source); // 到源碼列表中查到源代碼 const smContent = consumer.sourcesContent[smIndex]; // 將源代碼串按"行結束標記"拆分爲數組形式 const rawLines = smContent.split(/\r?\n/g); let begin = sm.line - offset; const end = sm.line + offset + 1; begin = begin < 0 ? 0 : begin; const context = rawLines.slice(begin, end).join('\n'); // 記得銷燬 consumer.destroy(); return { context, originLine: sm.line + 1, // line 是從 0 開始數,因此 +1 source: sm.source, } }; 複製代碼
你們根據 SourceMap
文件的格式,就能很好的理解這段代碼了。