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

最近在研究前端監控方案,因爲工做須要研究了下鵝廠的badjs源碼,主要是看了前端上報這一塊,也就是badjs-report。關於badjs的使用能夠看下官方文檔javascript

前端監控痛點

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

  1. 錯誤攔截
  2. 上報錯誤
  3. 離線錯誤日誌存儲
  4. 錯誤路徑回放
  5. 日誌可視化管理後臺
  6. 壓縮單行文件的源碼定位
  7. 郵箱(短信)提醒

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

BJ_REPORT.init初始化

badjs-report是在全局對象中插入BJ_REPORT對象,它提供了init()來進行初始化,該函數方法接受一個對象做爲配置參數。git

首先是將傳入的配置參數對象的值覆蓋私有_config對象的值。github

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

接着拼接上報url和清空錯誤緩存。web

// 沒有設置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()。bash

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。app

先根據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給我~~

相關文章
相關標籤/搜索