本文大體圍繞下面幾點展開討論:
javascript
對於 Javascript 而言,咱們面對的僅僅只是異常,異常的出現不會直接致使 JS 引擎崩潰,最多隻會使當前執行的任務終止。html
<script>
error
console.log('永遠不會執行');
</script>
<script>
console.log('我繼續執行')
</script>
複製代碼
在對腳本錯誤進行上報以前,咱們須要對異常進行處理,程序須要先感知到腳本錯誤的發生,而後再談異常上報。前端
腳本錯誤通常分爲兩種:語法錯誤,運行時錯誤。java
下面就談談幾種異常監控的處理方式:webpack
try-catch 異常處理ios
try-catch 在咱們的代碼中常常見到,經過給代碼塊進行 try-catch 進行包裝後,當代碼塊發生出錯時 catch 將能捕捉到錯誤的信息,頁面也將能夠繼續執行。git
可是 try-catch 處理異常的能力有限,只能捕獲捉到運行時非異步錯誤,對於語法錯誤和異步錯誤就顯得無能爲力,捕捉不到。github
示例:運行時錯誤web
try {
error // 未定義變量
} catch(e) {
console.log('我知道錯誤了');
console.log(e);
}
複製代碼
然而對於語法錯誤和異步錯誤就捕捉不到了。ajax
示例:語法錯誤
try {
var error = 'error'; // 大寫分號
} catch(e) {
console.log('我感知不到錯誤');
console.log(e);
}
複製代碼
通常語法錯誤在編輯器就會體現出來,常表現的錯誤信息爲:
這樣。可是這種錯誤會直接拋出異常,常使程序崩潰,通常在編碼時候容易觀察獲得。
示例:異步錯誤
try {
setTimeout(() => {
error // 異步錯誤
})
} catch(e) {
console.log('我感知不到錯誤');
console.log(e);
}
複製代碼
除非你在 setTimeout 函數中再套上一層 try-catch,不然就沒法感知到其錯誤,但這樣代碼寫起來比較囉嗦。
window.onerror 異常處理
window.onerror 捕獲異常能力比 try-catch 稍微強點,不管是異步仍是非異步錯誤,onerror 都能捕獲到運行時錯誤。
示例:運行時同步錯誤
/**
* @param {String} msg 錯誤信息
* @param {String} url 出錯文件
* @param {Number} row 行號
* @param {Number} col 列號
* @param {Object} error 錯誤詳細信息
*/
window.onerror = function (msg, url, row, col, error) {
console.log('我知道錯誤了');
console.log({
msg, url, row, col, error
})
return true;
};
error;
複製代碼
示例:異步錯誤
window.onerror = function (msg, url, row, col, error) {
console.log('我知道異步錯誤了');
console.log({
msg, url, row, col, error
})
return true;
};
setTimeout(() => {
error;
});
複製代碼
然而 window.onerror 對於語法錯誤仍是無能爲力,因此咱們在寫代碼的時候要儘量避免語法錯誤的,不過通常這樣的錯誤會使得整個頁面崩潰,仍是比較容易可以察覺到的。
在實際的使用過程當中,onerror 主要是來捕獲預料以外的錯誤,而 try-catch 則是用來在可預見狀況下監控特定的錯誤,二者結合使用更加高效。
須要注意的是,window.onerror 函數只有在返回 true 的時候,異常纔不會向上拋出,不然即便是知道異常的發生控制檯仍是會顯示
。
關於 window.onerror 還有兩點須要值得注意
當咱們遇到
報 404 網絡請求異常的時候,onerror 是沒法幫助咱們捕獲到異常的。
<script>
window.onerror = function (msg, url, row, col, error) {
console.log('我知道異步錯誤了');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
<img src="./404.png">
複製代碼
因爲網絡請求異常不會事件冒泡,所以必須在捕獲階段將其捕捉到才行,可是這種方式雖然能夠捕捉到網絡請求的異常,可是沒法判斷 HTTP 的狀態是 404 仍是其餘好比 500 等等,因此還須要配合服務端日誌才進行排查分析才能夠。
<script>
window.addEventListener('error', (msg, url, row, col, error) => {
console.log('我知道 404 錯誤了');
console.log(
msg, url, row, col, error
);
return true;
}, true);
</script>
<img src="./404.png" alt="">
複製代碼
這點知識仍是須要知道,要否則用戶訪問網站,圖片 CDN 沒法服務,圖片加載不出來而開發人員沒有察覺就尷尬了。
Promise 錯誤
經過 Promise 能夠幫助咱們解決異步回調地獄的問題,可是一旦 Promise 實例拋出異常而你沒有用 catch 去捕獲的話,onerror 或 try-catch 也無能爲力,沒法捕捉到錯誤。
window.addEventListener('error', (msg, url, row, col, error) => {
console.log('我感知不到 promise 錯誤');
console.log(
msg, url, row, col, error
);
}, true);
Promise.reject('promise error');
new Promise((resolve, reject) => {
reject('promise error');
});
new Promise((resolve) => {
resolve();
}).then(() => {
throw 'promise error'
});
複製代碼
雖然在寫 Promise 實例的時候養成最後寫上 catch 函數是個好習慣,可是代碼寫多了就容易糊塗,忘記寫 catch。
因此若是你的應用用到不少的 Promise 實例的話,特別是你在一些基於 promise 的異步庫好比 axios 等必定要當心,由於你不知道何時這些異步請求會拋出異常而你並無處理它,因此你最好添加一個 Promise 全局異常捕獲事件 unhandledrejection。
window.addEventListener("unhandledrejection", function(e){
e.preventDefault()
console.log('我知道 promise 的錯誤了');
console.log(e.reason);
return true;
});
Promise.reject('promise error');
new Promise((resolve, reject) => {
reject('promise error');
});
new Promise((resolve) => {
resolve();
}).then(() => {
throw 'promise error'
});
複製代碼
固然,若是你的應用沒有作 Promise 全局異常處理的話,那極可能就像某乎首頁這樣:
監控拿到報錯信息以後,接下來就須要將捕捉到的錯誤信息發送到信息收集平臺上,經常使用的發送形式主要有兩種:
實例 - 動態建立 img 標籤進行上報
function report(error) {
var reportUrl = 'http://xxxx/report';
new Image().src = reportUrl + 'error=' + error;
}
複製代碼
監控上報常見問題
Script error 腳本錯誤是什麼
由於咱們在線上的版本,常常作靜態資源 CDN 化,這就會致使咱們常訪問的頁面跟腳本文件來自不一樣的域名,這時候若是沒有進行額外的配置,就會容易產生 Script error。
可經過
查看效果。
Script error 是瀏覽器在同源策略限制下產生的,瀏覽器處於對安全性上的考慮,當頁面引用非同域名外部腳本文件時中拋出異常的話,此時本頁面是沒有權利知道這個報錯信息的,取而代之的是輸出 Script error 這樣的信息。
這樣作的目的是避免數據泄露到不安全的域中,舉個簡單的例子,
<script src="xxxx.com/login.html"></script>
複製代碼
上面咱們並無引入一個 js 文件,而是一個 html,這個 html 是銀行的登陸頁面,若是你已經登陸了,那 login 頁面就會自動跳轉到
,若是未登陸則跳轉到
,那麼報錯也會是
,經過這些信息能夠判斷一個用戶是否登陸他的賬號,給入侵者提供了十分便利的判斷渠道,這是至關不安全的。
介紹完背景後,那麼咱們應該去解決這個問題?
首先能夠想到的方案確定是同源化策略,將 JS 文件內聯到 html 或者放到同域下,雖然能簡單有效地解決 script error 問題,可是這樣沒法利用好文件緩存和 CDN 的優點,不推薦使用。正確的方法應該是從根本上解決 script error 的錯誤。
跨源資源共享機制( CORS )
首先爲頁面上的 script 標籤添加 crossOrigin 屬性
// http://localhost:8080/index.html
<script>
window.onerror = function (msg, url, row, col, error) {
console.log('我知道錯誤了,也知道錯誤信息');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>
// http://localhost:8081/test.js
setTimeout(() => {
console.log(error);
});
複製代碼
當你修改完前端代碼後,你還須要額外給後端在響應頭裏加上
,這裏我以 Koa 爲例。
const Koa = require('koa');
const path = require('path');
const cors = require('koa-cors');
const app = new Koa();
app.use(cors());
app.use(require('koa-static')(path.resolve(__dirname, './public')));
app.listen(8081, () => {
console.log('koa app listening at 8081')
});
複製代碼
讀者可經過
詳細的跨域知識我就不展開了,有興趣能夠看看我以前寫的文章:跨域,你須要知道的全在這裏
你覺得這樣就完了嗎?並無,下面就說一些 Script error 你不常碰見的點:
咱們都知道 JSONP 是用來跨域獲取數據的,而且兼容性良好,在一些應用中仍然會使用到,因此你的項目中可能會用這樣的代碼:
// http://localhost:8080/index.html
window.onerror = function (msg, url, row, col, error) {
console.log('我知道錯誤了,但不知道錯誤信息');
console.log({
msg, url, row, col, error
})
return true;
};
function jsonpCallback(data) {
console.log(data);
}
const url = 'http://localhost:8081/data?callback=jsonpCallback';
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
複製代碼
由於返回的信息會當作腳本文件來執行,一旦返回的腳本內容出錯了,也是沒法捕捉到錯誤的信息。
解決辦法也不難,跟以前同樣,在添加動態添加腳本的時候加上 crossOrigin,而且在後端配上相應的 CORS 字段便可.
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
複製代碼
讀者能夠經過
查看效果
知道原理以後你可能會以爲沒什麼,不就是給每一個動態生成的腳本添加 crossOrigin 字段嘛,可是在實際工程中,你多是面向不少庫來編程,好比使用 jQuery,Seajs 或者 webpack 來異步加載腳本,許多庫封裝了異步加載腳本的能力,以 jQeury 爲例你多是這樣來觸發異步腳本。
$.ajax({
url: 'http://localhost:8081/data',
dataType: 'jsonp',
success: (data) => {
console.log(data);
}
})
複製代碼
假如這些庫中沒有提供 crossOrigin 的能力的話(jQuery jsonp 可能有,僞裝你不知道),那你只能去修改人家寫的源代碼了,因此我這裏提供一個思路,就是去劫持 document.createElement,從根源上去爲每一個動態生成的腳本添加 crossOrigin 字段。
document.createElement = (function() {
const fn = document.createElement.bind(document);
return function(type) {
const result = fn(type);
if(type === 'script') {
result.crossOrigin = 'anonymous';
}
return result;
}
})();
window.onerror = function (msg, url, row, col, error) {
console.log('我知道錯誤了,也知道錯誤信息');
console.log({
msg, url, row, col, error
})
return true;
};
$.ajax({
url: 'http://localhost:8081/data',
dataType: 'jsonp',
success: (data) => {
console.log(data);
}
})
複製代碼
效果也是同樣的,讀者能夠經過
來查看效果:
這樣重寫 createElement 理論上沒什麼問題,可是入侵了本來的代碼,不保證必定不會出錯,在工程上仍是須要多嘗試下看看再使用,可能存在兼容性上問題,若是你以爲會出現什麼問題的話也歡迎留言討論下。
關於 Script error 的問題就寫到這裏,若是你理解了上面的內容,基本上絕大部分的 Script error 都能迎刃而解。
window.onerror 可否捕獲 iframe 的錯誤
當你的頁面有使用 iframe 的時候,你須要對你引入的 iframe 作異常監控的處理,不然一旦你引入的 iframe 頁面出現了問題,你的主站顯示不出來,而你卻渾然不知。
首先須要強調,父窗口直接使用 window.onerror 是沒法直接捕獲,若是你想要捕獲 iframe 的異常的話,有分好幾種狀況。
若是你的 iframe 頁面和你的主站是同域名的話,直接給 iframe 添加 onerror 事件便可。
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
window.frames[0].onerror = function (msg, url, row, col, error) {
console.log('我知道 iframe 的錯誤了,也知道錯誤信息');
console.log({
msg, url, row, col, error
})
return true;
};
</script>
複製代碼
讀者能夠經過
查看效果:
若是你嵌入的 iframe 頁面和你的主站不是同個域名的,可是 iframe 內容不屬於第三方,是你能夠控制的,那麼能夠經過與 iframe 通訊的方式將異常信息拋給主站接收。與 iframe 通訊的方式有不少,經常使用的如:postMessage,hash 或者 name 字段跨域等等,這裏就不展開了,感興趣的話能夠看:跨域,你須要知道的全在這裏
若是是非同域且網站不受本身控制的話,除了經過控制檯看到詳細的錯誤信息外,沒辦法捕獲,這是出於安全性的考慮,你引入了一個百度首頁,人家頁面報出的錯誤憑啥讓你去監控呢,這會引出不少安全性的問題。
壓縮代碼如何定位到腳本異常位置
線上的代碼幾乎都通過了壓縮處理,幾十個文件打包成了一個並醜化代碼,當咱們收到
的時候,咱們根本不知道這個變量 a 到底是什麼含義,此時報錯的錯誤日誌顯然是無效的。
第一想到的辦法是利用 sourcemap 定位到錯誤代碼的具體位置,詳細內容能夠參考:Sourcemap 定位腳本錯誤
另外也能夠經過在打包的時候,在每一個合併的文件之間添加幾行空格,並相應加上一些註釋,這樣在定位問題的時候很容易能夠知道是哪一個文件報的錯誤,而後再經過一些關鍵詞的搜索,能夠快速地定位到問題的所在位置。
收集異常信息量太多,怎麼辦
若是你的網站訪問量很大,假如網頁的 PV 有 1kw,那麼一個必然的錯誤發送的信息就有 1kw 條,咱們能夠給網站設置一個採集率:
Reporter.send = function(data) {
// 只採集 30%
if(Math.random() < 0.3) {
send(data) // 上報錯誤信息
}
}
複製代碼
這個採集率能夠經過具體實際的狀況來設定,方法多樣化,可使用一個隨機數,也能夠具體根據用戶的某些特徵來進行斷定。
上面差很少是我對前端代碼監控的一些理解,提及來容易,可是一旦在工程化運用,不免須要考慮到兼容性等種種問題,讀者能夠經過本身的具體狀況進行調整,前端代碼異常監控對於咱們的網站的穩定性起着相當重要的做用。如若文中全部不對的地方,還望指正。