最近在研究前端监控方案,由于工作需要研究了下鹅厂的badjs源码,主要是看了前端上报这一块,也就是badjs-report。关于badjs的使用可以看下
前端监控痛点
了解一个框架或者库之前要先思考它想解决的是什么问题。这篇文章比较详细地总结了前端监控所需要解决的问题,总结了下有:
- 错误拦截
- 上报错误
- 离线错误日志存储
- 错误路径回放
- 日志可视化管理后台
- 压缩单行文件的源码定位
- 邮箱(短信)提醒
上面的功能除了第四点和第六点,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给我~~
写在最后
我个人开了一个公众号“前端搬运小工”,我会定期推送优秀的前端精选文章,拒绝无脑基础入门的文章,带给你不一样的前端视角。