捕獲頁面中全域性Javascript異常
1 使用window.onError
瀏覽器提供了全域性的onError函式,我們可以使用它蒐集頁面上的錯誤
windowonerror = function(message, source, lineno, colno, error) { ... }
其中mesage為異常基本資訊,source為發生異常Javascript檔案url,lineno為發生錯誤的行號,我們可以通過error.stack獲取異常的堆疊資訊。下面是chrome中通過window.onError捕獲的錯誤例子:
message: Uncaught ReferenceError: test is not defined source: http://test.com/release/attach.js lineno: 16144 colno: 6 error: ReferenceError: test is not defined at http://test.com/release/attach.js:16144:6 at HTMLDocument.<anonymous> (http://test.com/release/vendor.js:654:71)
這種方式看似完美,其實有一個致命的問題。有些瀏覽器為了安全方面的考慮,對於不同域的Javascript檔案,通過window.onError無法獲取有效的錯誤資訊。比如firefox的錯誤訊息只有Script
error
,而且無法獲得確切的行號,更沒有錯誤堆疊資訊:
message: Script error.
source: "http://test.com/release/attach.js
lineno: 0
colno: 0
error: null
為了使得瀏覽器針對window.onError的跨域保護失效, 我們可以在靜態資源伺服器或者CDN的HTTP頭中加上如下允許跨域提示:
Access-Control-Allow-Origin: *
並在引用Javascript指令碼是加上crossorigin屬性:
<script crossorigin src=""></script>
完成上述兩步後,我們就可以方便的使用window.onError進行全域性異常捕獲,並獲取豐富的異常資訊了。但是有時對於第三方的CDN,我們無法新增跨域相關的頭資訊,下面我們就討論針這種情況的全域性Javascript異常捕獲方法。
2 使用AST為所有函式加上try catch
上文中提到了使用window.onError進行瀏覽器全域性異常捕獲,但是當我們無法新增跨域相關頭資訊時,window.onError就失效了。針對這種情況,我們可以對每一個函式新增try catch來捕獲函式內的異常,但是一個大型專案的函式太多,對每一個函式都手動新增try catch無疑是一個巨大的工作量。本文我們藉助AST(抽象語法樹)技術,對原始檔進行預處理,對每個函式自動的新增try catch。
語法樹是對原始碼最精確的表示,通過遍歷和操作語法樹,我們能夠精確的控制原始碼。生成JavaScript的AST是一件非常複雜的工作,本文暫時不打算涉及,好在UglifyJS已經有了完整的實現。
比如如下程式碼:
<script crossorigin src=""></script>
可以用語法樹表示:
通過使用Uglify提供的操作AST(抽象語法樹)的API,我們可以對每個函式新增try catch程式碼塊,並在catch中捕獲該函式的一切異常,下面是我的實現(請參考我的github:try-catch-global.js):
var fs = require('fs');
var _ = require('lodash');
var UglifyJS = require('uglify-js');
var isASTFunctionNode = function (node) {
return node instanceof UglifyJS.AST_Defun || node instanceof UglifyJS.AST_Function;
}
var globalFuncTryCatch = function (inputCode, errorHandler) {
if(!_.isFunction(errorHandler)){
throw 'errorHandler should be a valid function';
}
var errorHandlerSource = errorHandler.toString();
var errorHandlerAST = UglifyJS.parse('(' + errorHandlerSource + ')(error);');
var tryCatchAST = UglifyJS.parse('try{}catch(error){}');
var inputAST = UglifyJS.parse(inputCode);
var topFuncScope = [];
//將錯誤處理函式包裹進入catch中
tryCatchAST.body[0].bcatch.body[0] = errorHandlerAST;
//蒐集所有函式
var walker = new UglifyJS.TreeWalker(function (node) {
if (isASTFunctionNode(node)) {
topFuncScope.push(node);
}
});
inputAST.walk(walker);
//對函式進行變換, 新增try catch語句
var transfer = new UglifyJS.TreeTransformer(null,
function (node) {
if (isASTFunctionNode(node) && _.includes(topFuncScope, node)) {
//函式內部程式碼蒐集
var stream = UglifyJS.OutputStream();
for (var i = 0; i < node.body.length; i++) {
node.body[i].print(stream)
}
var innerFuncCode = stream.toString();
//清除try catch中定義的多餘語句
tryCatchAST.body[0].body.splice(0, tryCatchAST.body[0].body.length);
//用try catch包裹函式程式碼
var innerTyrCatchNode = UglifyJS.parse(innerFuncCode, {toplevel: tryCatchAST.body[0]});
//獲取函式殼
node.body.splice(0, node.body.length);
//生成有try catch的函式
return UglifyJS.parse(innerTyrCatchNode.print_to_string(), {toplevel: node});
}
});
inputAST.transform(transfer);
var outputCode = inputAST.print_to_string({beautify: true});
return outputCode;
}
module.exports.globalFuncTryCatch = globalFuncTryCatch;
藉助於globalFuncTryCatch
,我們對每個函式進行自動化地新增try catch語句,並使用自定義的錯誤處理函式:
globalFuncTryCatch(inputCode, function (error) {
//此處是異常處理程式碼,可以上報並記錄日誌
console.log(error);
});
通過將globalFuncTryCatch功能整合到構建工具中,我們就可以對目標Javascript檔案進行try catch處理。
綜上所述:
當靜態資源伺服器可以新增Access-Control-Allow-Origin: *
時,我們可以直接使用window.onError進行全域性異常捕獲;當靜態資源伺服器不受控制,window.onError失效,我們可以藉助AST技術,自動化地對全部目標Javascript函式新增try
catch來捕獲所有異常。
參考文件