监控前端性能与异常
这是我参与「第五届青训营」笔记创作活动的第15天.上一篇文章前端监控(2) | 青训营笔记 - 掘金 (juejin.cn)中了解了前端监控的常见异常, 今天主要学习如何编写代码实现性能与异常的监控.
性能指标监控
利用Performance和PerformanceObserver接口可以监控到一些标淮的渲染性能数据.
/**
* 列举出性能指标对应的 entry type
* fp,fcp --> paint
* lcp --> largest-contentful-paint
* fip --> first-input
*/
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
// 1. 通过 PerformanceObserver 监听
const p = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log(entry);
}
})
// observe获取需要的性能值
p.observe({ entryTypes });
// 2. 也可以通过 window.performance 对象拿到 fp fcp 和 fip。
// 注意如果同步打印他们是取不到值的
// 渲染前已经同步执行了JS, 因此输出时还没获得值. 应该在渲染完之后输出
console.log(window.performance.getEntriesByType('paint'));
window.performance.getEntriesByType('first-input');
// 因此实战时优先从performance中拿,没有才会从PerformanceObserver拿
把监听能力封装成一个monitor
// 希望满足以下要求
// (1)起名字,唯一标识
// (2)具有监听能力
// (3)主动开启,而不是被动开启(创建出来就马上开启)
// (4)上报能力,把监听到的数据上报
function createPerfMonitor(report: ({ name: string, data: any }) => void) { // 高阶函数,函数里传入report函数
const name = 'performance';
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
// 调用start才会开启监听能力
function start() {
const p = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
report({ name, data: entry }); //上报数据
}
})
p.observe({ entryTypes });
}
// 监听的名字和start方法
return { name, start }
}
JS错误监控
利用EventTarget.addEventListener的error和unhandledrejection 可以监控到全局的JS错误。
一般在根节点或全局对象监听,即window,因此也可以改写成window.addEventListener
// 1. 监听 js 执行报错
window.addEventListener("error", (e) => { // e表示一个事件
// 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
if (e.error) {
console.log('caputure an error', e.error);
}
});
throw(new Error('test'));
执行上方代码可以得到下述输出。
test为错误名称,下面是错误堆栈
// 1. 监听 js 执行报错
window.addEventListener("error", (e) => { // e表示一个事件
// 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
if (e.error) {
console.log('caputure an error', e.error);
}
});
// 2. 监听 promise rejection
window.addEventListener("unhandledrejection", (e) => {
console.log('capture a unrejection', e);
});
Promise.reject('test');
执行上述代码,得到下面输出
将上面两个功能封装成一个monitor。封装过程的思路与性能指标中一致。
function createJsErrorMonitor(report: ({ name: string, data: any }) => void) {
const name = "js-error";
function start() {
window.addEventListener("error", (e) => {
if (e.error) {
// 错误类型,错误信息
report({ name, data: { type: e.type, message: e.message } });
}
});
window.addEventListener("unhandledrejection", (e) => {
// 错误类型,错误原因。通过type来区分两类错误
report({ name, data: { type: e.type, reason: e.reason } });
});
}
return { name, start }
}
静态资源错误监控
利用window.addEventListener的error事件可以监控到静态资源错误,注意要和JS error进行区分。
静态资源报错在属性上具有明显的特征,若target/ srcElement 属性是空的,则不是静态资源。
// 1. 监控静态资源错误,注意需要在捕获阶段才能监听到
window.addEventListener('error', e => {
// 区分 js error
const target = e.target || e.srcElement; //为了兼容新旧浏览器,加上srcElement
// 非静态
if (!target) {
return
}
// target继承HTMLElement,可能是通过link/script标签创建的静态资源,要区分
if (target instanceof HTMLElement) {
let url; //静态资源指向地址
// 区分 link 标签,获取静态资源地址
if (target.tagName.toLowerCase() === 'link') {
// Link
url = target.getAttribute('href');
} else {
// Script
url = target.getAttribute('src');
}
console.log('异常的资源', url); //出错的静态资源url
}
}, true)
const link = document.createElement("link");
link.href = "1.css";
link.rel = "stylesheet";
document.head.append(link);
const script = document.createElement("script");
script.src = "2.js";
document.head.append(script);
注意:监控静态资源错误,注意需要在捕获阶段才能监听到。但是addEventListener的useCapture属性默认为false,因此要主动设置为true。
封装成monitor
function createResourceErrorMonitor(report: ({ name: string, data: any }) => void) {
const name = "resource-error";
function start() {
window.addEventListener('error', e => {
// 注意区分 js error
const target = e.target || e.srcElement;
if (!target) {
return
}
if (target instanceof HTMLElement) {
let url;
// 区分 link 标签,获取静态资源地址
if (target.tagName.toLowerCase() === 'link') {
url = target.getAttribute('href');
} else {
url = target.getAttribute('src');
}
report({ name, data: { url } });
}
}, true)
}
return { name, start }
}
请求异常监控
通过hook XMLHttpRequest,xhr 和fetch对象来监听请求时发生的错误。
- 写一个简易的 hook(钩子) 函数
想在原有的逻辑上注入自己的逻辑,从而实现监听,注入等能力。
function hookMethod(//高阶函数
//三个参数
obj: any,
key: string,
hookFunc: Function,
) {
//返回值也是一个函数
return (...params: any[]) => {
obj[key] = hookFunc(obj[key], ...params)
}
}
xhr : open-> send 两步完成后请求才生效
- hook xhr 对象的 open 方法拿到请求地址和方法
在原型对象上做hook
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
function (method: string, url: string) { // 即原始open的入参
// this指向原始xhr
this.payload = {
method,
url,
};
// 执行原函数(保证在执行自己逻辑时不破坏原逻辑)
origin.apply(this, [method, url]);
}
)();
- hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
function (...params: any[]) {
this.addEventListener("readystatechange", function () {
// ==4:请求完成,状态码>=400发生异常
if (this.readyState === 4 && this.status >= 400) {
this.payload.status = this.status;//获取状态码
console.log(this.payload);//异常情况的上下文
}
});
origin.apply(this, params);
}
)();
const xhr = new XMLHttpRequest();
xhr.open("post", "111.cc");
xhr.send();
- 封装成monitor
function createXhrMonitor(report: ({ name: string, data: any }) => void) {
const name = "xhr-error";
// hook
function hookMethod(
obj: any,
key: string,
hookFunc: Function,
) {
return (...params: any[]) => {
obj[key] = hookFunc(obj[key], ...params)
}
}
function start() {
// get
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
function (this, method: string, url: string) {
this.payload = {
method,
url,
};
origin.apply(this, [method, url]);
}
)();
//send
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
function (this, ...params: any[]) {
this.addEventListener("readystatechange", function () {
if (this.readyState === 4 && this.status >= 400) {
this.payload.status = this.status;
report({ name, data: this.payload });
}
});
origin.apply(this, ...params);
}
)();
}
// 返回start
return { name, start }
}