1. 程式人生 > >捕獲頁面中全域性Javascript異常

捕獲頁面中全域性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>

可以用語法樹表示:

a ast demo

通過使用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來捕獲所有異常。

參考文件