淺析badjs原始碼(前端監控方案)

詹前鑫發表於2018-07-09

最近在研究前端監控方案,由於工作需要研究了下鵝廠的badjs原始碼,主要是看了前端上報這一塊,也就是badjs-report。關於badjs的使用可以看下官方文件

前端監控痛點

瞭解一個框架或者庫之前要先思考它想解決的是什麼問題。前端異常監控系統的落地這篇文章比較詳細地總結了前端監控所需要解決的問題,總結了下有:

  1. 錯誤攔截
  2. 上報錯誤
  3. 離線錯誤日誌儲存
  4. 錯誤路徑回放
  5. 日誌視覺化管理後臺
  6. 壓縮單行檔案的原始碼定位
  7. 郵箱(簡訊)提醒

上面的功能除了第四點和第六點,badjs2都已經實現到。其中錯誤攔截、上報錯誤和離線錯誤日誌儲存是由前端元件badjs-report來實現的。而badjs-report的程式碼主要有三大入口:init初始化、onerror改寫和reportOfflinelog上報離線日誌。下面將一一介紹這三大入口如何呼叫其他函式並實現功能(限於篇幅限制,下面貼的程式碼有所刪減,可結合原始碼理解)。

BJ_REPORT.init初始化

badjs-report是在全域性物件中插入BJ_REPORT物件,它提供了init()來進行初始化,該函式方法接受一個物件作為配置引數。

首先是將傳入的配置引數物件的值覆蓋私有_config物件的值。

init: function(config) {
	if (T.isOBJ(config)) {
		// 遍歷覆蓋
        for (var key in config) {
            _config[key] = config[key];
        }
    }
}
複製程式碼

接著拼接上報url和清空錯誤快取。

// 沒有設定id將不上報
var id = parseInt(_config.id, 10);
if (id) {
    _config._reportUrl = (_config.url || "/badjs") +
        "?id=" + id +
        "&uin=" + _config.uin +
        "&";
}
// 清空錯誤列表,_process_log函式會在下面講到
if (_log_list.length) {
	_process_log();
}
複製程式碼

接著初始化indexedDB資料庫。badjs是將離線日誌資訊儲存於indexedDB資料庫中,然後通過呼叫reportOfflineLog()方法來上傳離線日誌。

if (!Offline_DB._initing) {
    Offline_DB._initing = true;
    Offline_DB.ready(function(err, DB) {
        if (DB) {
            setTimeout(function() {
		        // 清除過期日誌
                DB.clearDB(_config.offlineLogExp);
                setTimeout(function() {
                    _config.offlineLogAuto && _autoReportOffline();
                }, 5000);
            }, 1000);
        }

    });
}
複製程式碼

Offline_DB.ready()的主要工作是開啟資料庫並設定success和upgradeneeded監聽事件

// 開啟資料庫
var request = window.indexedDB.open("badjs", version);

// 開啟成功
request.onsuccess = function(e) {
    self.db = e.target.result;
    // 開啟成功後執行回撥
    setTimeout(function() {
        callback(null, self);
    }, 500);
};
// 版本升級(初始化時會先觸發upgradeneeded,再觸發success)
request.onupgradeneeded = function(e) {
   var db = e.target.result;
   if (!db.objectStoreNames.contains('logs')) {
       db.createObjectStore('logs', { autoIncrement: true });
   }
};
複製程式碼

改寫onerror

在BJreport初始化後就需要來改寫window.onerror,以便捕獲到程式發生的錯誤。重寫後的onerror主要是格式化錯誤資訊,並把錯誤push進錯誤佇列中,同時push()方法也會觸發_process_log()。

var orgError = global.onerror;
global.onerror = function(msg, url, line, col, error) {
    var newMsg = msg;
	// 格式化錯誤資訊
    if (error && error.stack) {
        newMsg = T.processStackMsg(error);
    }
    if (T.isOBJByType(newMsg, "Event")) {
        newMsg += newMsg.type ?
            ("--" + newMsg.type + "--" + (newMsg.target ?
                (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : "";
    }
    // 將錯誤資訊物件推入錯誤佇列中,執行_process_log方法進行上報
    report.push({
        msg: newMsg,
        target: url,
        rowNum: line,
        colNum: col,
        _orgMsg: msg
    });

    _process_log();
    // 呼叫原有的全域性onerror事件
    orgError && orgError.apply(global, arguments);
};
複製程式碼

badjs上報的功能主要通過_process_log()來實現,有隨機上報、忽略上報、離線日誌儲存和延遲上報。首先在push的時候會把錯誤物件push進_log_list,然後_process_log()會迴圈清空_log_list。

先根據config的random來決定是否忽略該次上報

// 取隨機數,來決定是否忽略該次上報
var randomIgnore = Math.random() >= _config.random;
複製程式碼

每次迴圈時先判斷是否超過重複上報數

// 重複上報
if (T.isRepeat(report_log)) continue;
複製程式碼

然後按照使用者定義的ignore規則進行篩選

// 格式化log資訊
var log_str = _report_log_tostring(report_log, submit_log_list.length);
// 若使用者自定義了ignore規則,則按照規則進行篩選
if (T.isOBJByType(_config.ignore, "Array")) {
    for (var i = 0, l = _config.ignore.length; i < l; i++) {
        var rule = _config.ignore[i];
        if ((T.isOBJByType(rule, "RegExp") && rule.test(log_str[1])) ||
            (T.isOBJByType(rule, "Function") && rule(report_log, log_str[1]))) {
            isIgnore = true;
            break;
        }
    }
}
複製程式碼

接著將離線日誌存入資料庫,將需要上報的日誌push進submit_log_list

// 通過了ignore規則
if (!isIgnore) {
    // 若離線日誌功能已開啟,則將日誌存入資料庫
    _config.offlineLog && _save2Offline("badjs_" + _config.id + _config.uin, report_log);
    // level為20表示是offlineLog方法push進來的,只存入離線日誌而不上報
    if (!randomIgnore && report_log.level != 20) {
        // 若可以上報,則推入submit_log_list,稍後由_submit_log方法來清空該佇列並上報
        submit_log_list.push(log_str[0]);
        // 執行上報回撥函式
        _config.onReport && (_config.onReport(_config.id, report_log));
    }

}
複製程式碼

迴圈結束後根據需要進行上報或者延遲上報

if (isReportNow) {
  _submit_log(); // 立即上報
} else if (!comboTimeout) {
    comboTimeout = setTimeout(_submit_log, _config.delay); // 延遲上報
}
複製程式碼

在_submit_log()方法中,採用的是new一個img標籤來進行上報

var _submit_log = function() {
    // 若使用者自定義了上報方法,則使用自定義方法
    if (_config.submit) {
        _config.submit(url, submit_log_list);
    } else {
        // 否則使用img標籤上報
        var _img = new Image();
        _img.src = url;
    }
    submit_log_list = [];
};
複製程式碼

上傳離線日誌

badjs需要使用者主動呼叫BJ_REPORT.reportOfflineLog()方法來上傳資料庫中的離線日誌。

reportOfflineLog()方法首先是呼叫Offline_DB.ready開啟資料庫,然後在回撥中通過DB.getLogs()來獲取到資料庫中的日誌,最後通過form表單提交來上傳資料。

reportOfflineLog: function() {
    Offline_DB.ready(function(err, DB) {
        // 日期要求是startDate ~ endDate
        var startDate = new Date - 0 - _config.offlineLogExp * 24 * 3600 * 1000;
        var endDate = new Date - 0;
        DB.getLogs({
            start: startDate,
            end: endDate,
            id: _config.id,
            uin: _config.uin
        }, function(err, result) {
            var iframe = document.createElement("iframe");
            iframe.name = "badjs_offline_" + (new Date - 0);
            iframe.frameborder = 0;
            iframe.height = 0;
            iframe.width = 0;
            iframe.src = "javascript:false;";

            iframe.onload = function() {
                var form = document.createElement("form");
                form.style.display = "none";
                form.target = iframe.name;
                form.method = "POST";
                form.action = _config.offline_url || _config.url.replace(/badjs$/, "offlineLog");
                form.enctype.method = 'multipart/form-data';

                var input = document.createElement("input");
                input.style.display = "none";
                input.type = "hidden";
                input.name = "offline_log";
                input.value = JSON.stringify({ logs: result, userAgent: navigator.userAgent, startDate: startDate, endDate: endDate, id: _config.id, uin: _config.uin });
                iframe.contentDocument.body.appendChild(form);
                form.appendChild(input);
                // 通過form表單提交來上報離線日誌
                form.submit();

                setTimeout(function() {
                    document.body.removeChild(iframe);
                }, 10000);

                iframe.onload = null;
            };
            document.body.appendChild(iframe);
        });
    });
}
複製程式碼

結語

為了防止篇幅過長,上述原始碼我做了一些刪減,如果想看完整原始碼可以看下我自己加了中文註釋的版本https://github.com/Q-Zhan/badjs-report-annotated,有任何問題都可以提issue給我~~

相關文章