在研发过程中,日志是非常重要的一环,它可以帮助我们快速定位问题,解决问题。在前端开发中,日志也是非常重要的一环,它可以帮助我们快速定位问题,解决问题。本文将介绍前端日志的规范和最佳实践。但是我们经常看到一些项目日志打得满天飞,但是到了真正定位问题的时候,发现日志并没有什么卵用。这是因为日志打得不规范,不规范的日志是没有意义的。所以我们需要规范日志的打印,才能让日志发挥最大的作用。
那么,我们首先就要思考一下,打印什么样的日志才是有助于定位前端问题的,我想,可以从我们真是定位用户反馈问题的场景来思考。
通常,前端用户反馈的问题大概有以下几种:
- 页面加载慢
- 页面渲染错乱
- 页面白屏等交互异常
- 页面崩溃
- 页面卡顿
下面,我们可以基于这些场景,来思考一下,我们应该打印什么样的日志,才能帮助我们快速定位问题。
业务异常日志
页面加载慢
对于页面加载慢,这个问题,我们可以通过performance
对象来获取页面加载的性能数据,然后打印出来,比如:
javascript
window.addEventListener('load', function() {
setTimeout(function() {
var timing = window.performance.timing;
var loadTime = timing.loadEventEnd - timing.navigationStart;
var pageUrl = window.location.href;
console.log('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');
}, 0);
});
这样,我们就可以在页面加载完成之后,打印出页面加载时间,这样我们就可以通过日志来定位页面加载慢的问题。
通常,日志里面可以穿插一些告警,比如,我们这里可以加上一个判断,如果页面加载时间超过了3秒,我们就打印一个告警,比如:
javascript
window.addEventListener('load', function() {
setTimeout(function() {
var timing = window.performance.timing;
var loadTime = timing.loadEventEnd - timing.navigationStart;
var pageUrl = window.location.href;
console.log('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');
if (loadTime > 3000) {
console.warn('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds, it is too slow.');
//todo 上报到监控系统
reportToMonitor('Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds, it is too slow.');
}
}, 0);
});
页面渲染错乱
对于页面渲染错乱,造成这个问题的原因有很多,比如网络问题、代码问题、浏览器兼容问题等等,这个问题比较复杂,我们可以通过一些手段来定位这个问题,比如:
这个问题,我们可以通过window.onerror
来做,从里面区出渲染错误的问题,比如:
javascript
window.onerror = function(message, source, lineno, colno, error) {
console.error('Error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);
//分析渲染错误
parseAndLogRenderError(message, source, lineno, colno, error);
};
// 举例:分析渲染错误
function parseAndLogRenderError(message, source, lineno, colno, error) {
if (message.indexOf('render error') > -1) {
console.error('Render error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);
//todo 上报到监控系统
reportToMonitor('Render error: ' + message + ' Script: ' + source + ' Line: ' + lineno + ' Column: ' + colno + ' StackTrace: ' + error.stack);
}
}
当然,window.onerror 只能捕获到一部分错误,我们还需要结合 window.addEventListener('error', function(event) {}) 来捕获一些资源加载错误,比如:
javascript
window.addEventListener('error', function(event) {
console.error('Error: ' + event.message + ' Script: ' + event.filename + ' Line: ' + event.lineno + ' Column: ' + event.colno + ' StackTrace: ' + event error.stack);
parseAndLogRenderError(event.message, event.filename, event.lineno, event.colno, event.error);
});
页面交互异常
这里的页面交互异常,通常是指用户在页面上进行一些操作的时候,出现了一些异常,比如点击按钮无反应、输入框无法输入等等,这个问题,我们可以通过一些手段来定位。
先说一说点击按钮无反应的问题。
假如说,我们有一个这样的场景,用户点击一个按钮,理论上点击按钮会发送一个请求,成功失败可能都会有一个界面上的反馈,但是如何点击之后,界面没有任何的反馈,这个时候就,我们基本上可以判定,这种时候就是页面交互异常了。那么,我们该如何捕捉这种异常呢?
我们可以通过window.addEventListener('click', function(event) {})
来捕捉用户的点击事件,然后在里面做一些判断,比如:
javascript
// 设置一个标志位,当按钮被点击时,将标志位设置为 true。
var isButtonClicked = false;
window.addEventListener('click', function(event) {
var target = event.target;
if (target.tagName === 'BUTTON') {
console.log('User clicked button: ' + target.innerText);
// 设置标志位
isButtonClicked = true;
}
});
// 创建一个 observer 实例
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
// 如果页面有变化并且按钮被点击过,重置标志位
// 这里的mutations 可能需要根据实际情况来判断,具体要结合自己业务逻辑来判断
if(mutation.type === 'childList' || mutation.type === 'attributes') {
if (isButtonClicked) {
console.log('Page change detected after button click.');
isButtonClicked = false;
}
}
});
});
// 配置观察选项:
var config = { attributes: true, childList: true, characterData: true, subtree: true };
// 传入目标节点和观察选项
observer.observe(document.body, config);
// 启动定时器
setInterval(function() {
if (isButtonClicked) {
console.log('Button click exception detected.');
// 上报到监控系统
reportToMonitor('Button click exception detected');
// 重置标志位
isButtonClicked = false;
}
}, 5000); // 5秒后检查标志位
页面白屏的问题
页面白屏的问题,通常是指用户打开页面之后,页面长时间没有任何的反应,这个问题,我们可以通过一些手段来定位。
我们可以通过window.addEventListener('DOMContentLoaded', function() {})
来捕捉页面的加载事件,然后在里面做一些判断,比如:
javascript
window.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (document.body.innerHTML === '') {
console.error('Page is blank.');
// 上报到监控系统
reportToMonitor('Page is blank.');
}
}, 3000); // 3秒后检查页面是否为空
});
或者,我们可以通过window.addEventListener('load', function() {})
来捕捉页面的加载事件,然后在里面做一些判断,比如:通过oberver监听页面变化,如果页面变化了,就说明页面没有白屏.
javascript
window.addEventListener('load', function() {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('Page change detected.');
// 上报到监控系统
reportToMonitor('Page change detected.');
});
});
var config = { attributes: true, childList: true, characterData: true, subtree: true };
observer.observe(document.body, config);
});
页面卡顿
页面卡顿的问题,通常是指用户在页面上进行一些操作的时候,页面出现了卡顿的现象,我们先来分析一下,页面卡顿的原因。
页面卡顿的原因,通常有以下几种:
- 页面渲染性能问题
- 页面交互性能问题
- 页面资源加载性能问题
- 页面网络性能问题 等等,不排除可能还有其他的原因,但是这里我们只列举了一些常见的原因。
- 内存泄漏,这通常是元凶
那么,我们该如何捕捉页面卡顿的问题呢?
我们可以通过window.requestAnimationFrame
来捕捉页面的渲染性能问题,比如:
javascript
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
var lastFrameTime = 0;
var frameCount = 0;
var frameRate = 0;
var frameRateThreshold = 60;
function onAnimationFrame() {
var now = Date.now();
var elapsed = now - lastFrameTime;
lastFrameTime = now;
frameCount++;
if (elapsed > 1000) {
frameRate = frameCount;
frameCount = 0;
if (frameRate < frameRateThreshold) {
console.warn('Frame rate is ' + frameRate + 'fps, it is too low.');
// 上报到监控系统
reportToMonitor('Frame rate is ' + frameRate + 'fps, it is too low.');
}
}
window.requestAnimationFrame(onAnimationFrame);
}
window.requestAnimationFrame(onAnimationFrame);
这样,我们就可以通过window.requestAnimationFrame
来捕捉页面的渲染性能问题。
对于内存泄漏,我们可以监听页面卸载事件,然后检查所有的事件处理器和定时器是否都已经被清除,比如:
javascript
// 存储所有的事件处理器和定时器
var eventHandlers = [];
var timers = [];
// 覆盖 addEventListener 和 removeEventListener 方法
var originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(eventName, eventHandler) {
eventHandlers.push({target: this, eventName: eventName, eventHandler: eventHandler});
originalAddEventListener.call(this, eventName, eventHandler);
};
var originalRemoveEventListener = EventTarget.prototype.removeEventListener;
EventTarget.prototype.removeEventListener = function(eventName, eventHandler) {
eventHandlers = eventHandlers.filter(function(handler) {
return handler.target !== this || handler.eventName !== eventName || handler.eventHandler !== eventHandler;
});
originalRemoveEventListener.call(this, eventName, eventHandler);
};
// 覆盖 setTimeout 和 clearTimeout 方法
var originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback, delay) {
var id = originalSetTimeout(callback, delay);
timers.push(id);
return id;
};
var originalClearTimeout = window.clearTimeout;
window.clearTimeout = function(id) {
timers = timers.filter(function(timer) {
return timer !== id;
});
originalClearTimeout(id);
};
// 在页面卸载时检查所有的事件处理器和定时器是否都已经被清除
window.addEventListener('unload', function() {
if (eventHandlers.length > 0) {
console.error('Memory leak detected: Not all event handlers were removed.');
// 上报到监控系统
reportToMonitor('Memory leak detected: Not all event handlers were removed.');
}
if (timers.length > 0) {
console.error('Memory leak detected: Not all timers were cleared.');
// 上报到监控系统
reportToMonitor('Memory leak detected: Not all timers were cleared.');
}
});
用户行为日志
用户行为日志是指用户在页面上的一些操作,比如点击按钮、输入框输入等等,这些操作都是用户行为日志,这些日志是非常重要的,它可以帮助我们快速定位问题。比如,用户反馈说,我点击了一个按钮,但是没有反应,这个时候,我们就可以通过用户行为日志来定位问题。但是通常用户将的可能是问题出现的时间点,而不是问题出现的原因,所以我们需要在用户行为日志中加入一些额外的信息。用户经历过哪些路由页面,做过哪些交互,各交互步骤的数据是否正常,这些都是我们需要记录的。
对于页面的路由,我们可以通过window.addEventListener('hashchange', function() {})
来捕捉页面的路由变化,然后在里面打印一些日志,比如:
javascript
window.addEventListener('hashchange', function() {
console.log('User navigated to ' + window.location.hash);
});
对于用户的点击事件,我们可以通过window.addEventListener('click', function(event) {})
来捕捉用户的点击事件,然后在里面打印一些日志,比如:
javascript
window.addEventListener('click', function(event) {
var target = event.target;
if (target.tagName === 'BUTTON') {
console.log('User clicked button: ' + target.innerText);
}
});
对于用户的输入事件,我们可以通过window.addEventListener('input', function(event) {})
来捕捉用户的输入事件,然后在里面打印一些日志,比如:
javascript
window.addEventListener('input', function(event) {
var target = event.target;
if (target.tagName === 'INPUT') {
console.log('User input: ' + target.value);
}
});
对于一些用户的交互事件,我们可以通过window.addEventListener('customEvent', function(event) {})
来捕捉用户的交互事件,然后在里面打印一些日志,比如:
javascript
// 触发自定义事件
window.dispatchEvent(new CustomEvent('customEvent', {detail: 'User interaction detected.'}));
// 监听自定义事件
window.addEventListener('customEvent', function(event) {
console.log('User custom event: ' + event.detail);
});
// 触发自定义事件,我们可以做一个工具函数,比如:这阿姨给你方便在任何地方触发自定义事件
function triggerCustomEvent(eventName, detail) {
window.dispatchEvent(new CustomEvent(eventName, {detail: detail}));
}
一些情况下,前端页面的交互是通过后端返回的数据来触发的,这个时候,数据的正确性也是非常重要的,我们可以通过window.addEventListener('ajaxSuccess', function(event) {})
来捕捉用户的交互事件,然后在里面打印一些日志,比如:
javascript
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
// 请求成功
// 请求成功
if (response.data.code !== 0) {
console.error('Request success, but response data is not as expected.');
triggerCustomEvent('request abnormal', 'Request success, but response data is not as expected.');
}
triggerCustomEvent('request Success', response.data);
return response;
}, function (error) {
// 请求失败
console.error(error);
triggerCustomEvent('request Error', 'Ajax request error.');
return Promise.reject(error);
});
日志规范
我们还差一些什么,我们怎么知道打一些日志,但是怎么评估这些日志是否非常容易帮助我们定位问题呢?换句话说,当用户反馈问题的时候,我们怎么知道我们的日志是否能帮助我们快速定位问题呢?
一个场景:用户A,通过反馈说,你们页面加载好慢啊。然后就没有其他信息了。这个时候,我们需要去从日志中发现一些问题。
我们会问用户一个问题,比如用户的uid,然后我们就可以通过uid去查找用户的日志,然后我们就可以通过用户的日志来定位问题。
那么,我们就需要在日志中加入一些用户的信息,比如用户的uid,用户的设备信息,用户的网络信息等等,这样我们就可以通过用户的日志来定位问题。因此,我们需要在日志中加入一些用户的信息,比如:
javascript
// 获取用户的uid
var uid = getUid();
// 获取用户的设备信息
var deviceInfo = getDeviceInfo();
// 获取用户的网络信息
var networkInfo = getNetworkInfo();
// 打印环境信息
console.log('User: ' + uid + ' Device: ' + deviceInfo + ' Network: ' + networkInfo + ' Page load time for ' + pageUrl + ' is ' + loadTime + ' milliseconds.');
日志上报
有了收集的日志之后,我们上报到日志系统,那么这个日志系统应该是怎么样的呢?现在,我们可以使用 mermaid 来绘制一下整个日志系统的架构图:
这块后端可能有一些开源的日志系统,比如 ELK、Logstash、Kibana、Prometheus、Grafana 等等,这些都是比较常见的日志系统,我们可以根据自己的需求来选择。
比如,我们可以上报到ELK,然后通过Kibana来分析日志,通过Prometheus、Grafana来做一些监控。这些的搭建和使用,这里就不展开了。这里最终是需要提供前端日志的上报接口,然后后端来接收这些日志。
然后前端的日志上报,我们可以通过一些手段来做,比如:
javascript
function reportToMonitor(log) {
// 上报到监控系统
var img = new Image();
img.src = 'http://monitor.com/report?log=' + log;
}
这里为什么要用img
标签来上报日志呢?因为img
标签是不会阻塞页面的,而且可以跨域,这样我们就可以通过img
标签来上报日志。
另外,我们还可以通过navigator.sendBeacon
来上报日志,这个方法是异步的,不会阻塞页面,比如:
javascript
function reportToMonitor(log) {
// 上报到监控系统
navigator.sendBeacon('http://monitor.com/report', log);
}
然后,为了保证日志的可靠性,我们还可以通过localStorage
来存储日志,然后在下一次用户访问的时候,再上报日志,比如:
javascript
function reportToMonitor(log) {
// 上报到监控系统
if (navigator.sendBeacon) {
navigator.sendBeacon('http://monitor.com/report', log);
} else {
localStorage.setItem('log', log);
}
}
有些人可能会说,我还需要做一些日志的压缩和加密,这个时候,我们可以通过pako
来做日志的压缩,通过CryptoJS
来做日志的加密,比如:
javascript
function reportToMonitor(log) {
// 上报到监控系统
var compressedLog = pako.deflate(log, { to: 'string' });
var encryptedLog = CryptoJS.AES.encrypt(compressedLog, 'secret key').toString();
var img = new Image();
img.src = 'http://monitor.com/report?log=' + encryptedLog;
}
有人可能会讲,频繁的发送请求,可能会导致一些性能问题,能不能积攒一批日志,然后再发送呢?这个时候,我们可以通过setTimeout
来做,比如:
javascript
var logs = []; // 存储日志
function reportToMonitor(log) {
// 存储日志
logs.push(log);
// 延迟发送日志
setTimeout(function() {
var img = new Image();
img.src = 'http://monitor.com/report?log=' + logs.join(',');
logs = [];
}, 5000); // 5秒后发送日志
}
结构化日志
我们有了上述日志之后就,针对一个具体用户进行搜索,看到可能是这样的一些个日志:
- User: 123456 Device: iPhone 6s Network: 4G enter page /home 2024-02-12 12:00:00
- User: 123456 Device: iPhone 6s Network: 4G Page load time for /home is 3000 milliseconds 2024-02-12 12:00:03
- User: 123456 Device: iPhone 6s Network: 4G User clicked button: submit 2024-02-12 12:00:05
- User: 123456 Device: iPhone 6s Network: 4G User input[id=userName] : zhangsan 2024-02-12 12:00:06
- User: 123456 Device: iPhone 6s Network: 4G User input[id=password] : 123456 2024-02-12 12:00:07
- User: 123456 Device: iPhone 6s Network: 4G User clicked button: login 2024-02-12 12:00:08
- User: 123456 Device: iPhone 6s Network: 4G Request success, url: /login. 2024-02-12 12:00:12
- User: 123456 Device: iPhone 6s Network: 4G User navigated to /dashboard 2024-02-12 12:00:13
- User: 123456 Device: iPhone 6s Network: 4G Page load time for /dashboard is 3000 milliseconds 2024-02-12 12:00:16
- User: 123456 Device: iPhone 6s Network: 4G Request Failed, url: /dashboard. {errMessage: 'Internal Server Error'} 2024-02-12 12:00:20
这样,我们就可以通过用户的日志来定位问题。
日志图形化
比如,我们可以将用户的日志,使用 mermaid 来画一下这个流程图:
篇幅有限,顺着思路,本来想在写一些监控相关的内容,但是感觉篇幅有点长了,就到这里吧。后续在继续写一些监控相关的。
关注我的公众号,第一时间获取更新!