main.js
import monitor from '@/utils/monitor';
// 监控初始化
Vue.prototype.monitor = new monitor({cacheMax:10,Vue
})
monitor.js
import { metricsCollect, monitorUrl } from '@/api/monitor';
import { onLCP } from 'web-vitals';
export default class Monitor {config = {} //监控配置isStop = false //是否停止上报timer = null //定时器constructor(config) {Monitor.config = config;// this.caughtError(); // 捕获错误this.resetXhr(); // 重置xhr请求// this.resetFetch(); // 重置fetch请求// 页面加载完成上报window.addEventListener('load', () => { this.getWebPerformance();// 10分钟上报一次this.timer = setInterval(() => {this.toReport(null,true);}, 180000); });// 页面关闭上报window.onbeforeunload = () => {this.toReport()this.timer && clearInterval(this.timer);};// 页面切换上报document.addEventListener("visibilitychange", this.pageVisibilitychange());// 离线恢复上报window.addEventListener('online', this.networkOnline());}pageVisibilitychange(){if (document.visibilityState === 'visible') {this.toReport()}if (document.visibilityState === 'hidden') {this.toReport()}}networkOnline(){this.toReport()}getWebPerformance = () => {onLCP((metric) => {if (metric.value >= 3000) {this.toReport({subBizType: 'WhiteScreen',logType: 'info',collectTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),data: JSON.stringify({success: true,timeSpend: metric.value,})});}});}// getEnteries = () => {// console.log(window.performance.getEntriesByType('resource'), 'resource')// const resources = window.performance.getEntriesByType('resource');// return resources.map((item) => ({// resource: item.name,// duration: item.duration,// size: item.decodedBodySize,// type: item.initiatorType,// }));// }// 上报数据toReport = async (data,immediately = false) => {if (data) {const reportStack = localStorage.getItem('reportStack'); // 获取缓存数据if (!reportStack) {localStorage.setItem('reportStack', JSON.stringify([data]));} else {const reportData = JSON.parse(reportStack);localStorage.setItem('reportStack', JSON.stringify([...reportData, data]));}}if (this.isStop) return;const cacheMax = Monitor.config.cacheMax || 5;const reportStack = localStorage.getItem('reportStack') && JSON.parse(localStorage.getItem('reportStack')); // 获取缓存数据if ((reportStack?.length >= cacheMax) || (reportStack?.length > 0 && immediately )) {this.isStop = true;metricsCollect({sendTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),infos: reportStack}).then(() => {localStorage.setItem('reportStack', JSON.stringify([]));}).finally(() => {this.isStop = false;});}}resetXhr = () => {if (!window.XMLHttpRequest) return;const xmlhttp = window.XMLHttpRequest;const originOpen = xmlhttp.prototype.open;const originSend = xmlhttp.prototype.send;var that = this;xmlhttp.prototype.open = function (...args) {this.url = args[1]this.method = args[0]return originOpen.apply(this, args);};xmlhttp.prototype.send = function (...args) {//如果请求的URL是监控接口本身(monitorUrl),则不会处理,避免循环上报。if (this.url.indexOf(monitorUrl) === -1) {const xml = this;const url = this.url;const method = this.method;const isGet = method.toLocaleLowerCase() === 'get';const reqUrl = isGet ? url.split('?')[0] : url;let startTime;const originSetRequestHeader = this.setRequestHeader;const requestHeader = {};this.setRequestHeader = function (key, val) {requestHeader[key] = val;return originSetRequestHeader.apply(this, [key, val]);};xml.addEventListener('readystatechange', function (res) {if (this.readyState === XMLHttpRequest.DONE) {const cost = performance.now() - startTime;const status = this.status < 200 || this.status >= 300 || (this.status==200 && this.response.code !=200) ? 'error' : 'warn'let config = {subBizType: 'CommRequest',logType: cost >= 3000 ? 'warn' : status,collectTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),data: JSON.stringify({eventSource: reqUrl,timeSpend: cost,errorMsg: status == 'error' ? this.response : undefined,})};if (status < 200 || status >= 300 || cost >= 3000) {that.toReport(config);}}});xml.addEventListener('loadstart', function (data) {startTime = performance.now();});}return originSend.apply(this, args);};}resetFetch = () => {const oldFetch = window.fetch;window.fetch = (...args) => {const [url, { method, headers, body }] = args;const startTime = performance.now();const data = {type: 'request',url: url,method: method.toLocaleLowerCase(),reqHeaders: headers ? JSON.stringify(headers) : '',reqBody: body ? JSON.stringify(body) : '',status: 0,requestType: 'done',cost: 0,};return new Promise((resolve, reject) => {oldFetch.apply(window, args).then((res) => {const endTime = performance.now();data.cost = endTime - startTime;data.status = res.status;data.requestType = res.ok ? 'done' : 'error';this.toReport({type: 'request',...this.getUrlQuery(),data});resolve(res);}).catch((error) => {const endTime = performance.now();data.cost = endTime - startTime;data.status = 0;data.requestType = 'error';this.toReport({type: 'request',...this.getUrlQuery(),data});reject(error);});});};}getUrlQuery = () => {const isHash = location.hash;if (isHash) {const link = location.hash.replace('#', '');const [pageUrl, query] = link.split('?');return {pageUrl,query: query || '',domain: location.host,};}else {return {query: location.search.replace('?', '') || '',pageUrl: location.pathname,domain: location.host,};}}caughtError = () => {var that = this;// 监听js错误window.addEventListener('error', (error) => {console.log('jsError', error);if (error instanceof ErrorEvent) {that.toReport({...this.getUrlQuery(),type: 'jsError',message: error.message, //错误信息(字符串)stack: error.error.stack, //错误堆栈(字符串)colno: error.colno, //发生错误的列号(数字)lineno: error.lineno, //发生错误的行号(数字)filename: error.filename //发生错误的文件名(字符串)});console.log('jsError', error);} else {const { type, target } = error;that.toReport({...this.getUrlQuery(),type: 'loadResourceError',resourceType: type,resourceUrl: target.src});console.log('loadResourceError', error);}}, true);// 监听promise错误window.addEventListener('unhandledrejection', (error) => {if (error.reason?.name == "AxiosError") return;that.toReport({type: 'rejectError',reason: error.reason.toString(),...this.getUrlQuery()});});// 监听vue错误if (Monitor.config.Vue) {const vue = Monitor.config.Vue;vue.config.errorHandler = (err, vm, info) => {that.toReport({type: 'vueError',reason: err.toString(),// info,...this.getUrlQuery()});};}}getNetworkSpeed = () => {return new Promise((resolve, reject) => {let fileSize;let xhr = new XMLHttpRequest();let startTime;let endTime;let url = '';xhr.onreadystatechange = () => {if (xhr.readyState === 2) {startTime = Date.now();}if (xhr.readyState === 4 && xhr.status === 200) {endTime = Date.now();fileSize = xhr.responseText.length;//单位(KB/s)let speed = fileSize / ((endTime - startTime) / 1000) / 1024;speed = speed.toFixed(2);resolve({ downloadSpeed: speed, url });}}// 错误情况xhr.onerror = () => {reject('error');}xhr.open("GET", `${url}?rand=` + Math.random(), true);xhr.send();});}destory = () => {this.timer && clearInterval(this.timer);Monitor.config.Vue.prototype.monitor = null;window.removeEventListener('online', this.networkOnline);window.removeEventListener('visibilitychange', this.pageVisibilitychange);}
}
Web Vitals
Web Vitals是Google定义的良好网站的基本指标,核心包括LCP、FID和CLS这三个指标,分别对应加载速度、交互延迟和视觉稳定性。还提到了这些指标的标准值和如何通过web-vitals库进行测量。这里的信息很基础,应该作为回答的核心部分。
Web Vitals的三个核心指标,并补充了其意义,比如用户体验的量化、SEO影响等。可能需要在回答中提到这些指标的重要性,尤其是Google将其作为搜索排名的一部分,这对用户来说可能是个关键点。
web-vitals库的使用方法,包括安装、指标的具体含义以及如何集成到React项目中。这说明web-vitals不仅是一个概念,还有实际的工具支持,应该说明它是一个由Google提供的开源库,方便开发者测量这些指标。
Web Vitals作为新一代性能指标,并提到了一些扩展工具和Chrome插件,用于实时监控指标。这部分可以补充说明除了核心指标外,还有辅助工具帮助开发者更好地优化。
总结起来,回答需要涵盖以下几点:
- Web Vitals的定义和核心指标(LCP、FID、CLS),参考网页1、2、3。
- 每个指标的具体含义和标准值,结合网页1、2、5的描述。
- web-vitals库的作用和实际应用,如集成到项目、与分析工具结合,参考网页3、4、5、6。
- 这些指标的重要性和影响,比如用户体验、SEO优化,引用网页2和4的内容。
- 可能的辅助工具或扩展,如Chrome插件,来自网页3、7的信息。
Web Vitals 是 Google 提出的用于量化网页用户体验的核心性能指标体系,旨在简化开发者对网站健康度的评估。它通过三个核心指标(Core Web Vitals)和辅助指标,帮助优化加载速度、交互响应性和视觉稳定性。以下是具体解析:
一、核心指标与作用
-
LCP(最大内容渲染时间)
衡量页面从加载开始到视口内最大内容元素(如图片、标题文本块)完成渲染的时间。良好标准为 ≤2.5秒。若超时,用户可能因等待过久而流失,需优化资源加载或服务器响应。 -
FID(首次输入延迟)
记录用户首次与页面交互(点击、输入等)到浏览器实际响应的延迟时间。理想值为 ≤100毫秒。高延迟会导致用户感知卡顿,需减少主线程阻塞或优化代码执行效率。 -
CLS(累计布局偏移)
评估页面加载期间意外布局偏移的累积分数(0-1分)。优秀标准为 ≤0.1。常见于未预设尺寸的图片或动态插入内容,需通过占位符或预留空间避免布局跳动。
二、辅助工具与实现
• 测量工具:
Google 提供开源库 web-vitals
,支持通过 JavaScript 或 React 等框架集成。开发者可调用 getCLS()
、getFID()
等函数直接获取指标数据。
示例代码:
import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS(console.log); // 输出 CLS 数据
三、优化意义与影响
- 用户体验提升:直接关联用户对页面流畅度、稳定性的感知,降低跳出率。
- SEO 排名优化:Google 已将 Core Web Vitals 纳入搜索排名算法,优化后可提高网站搜索可见性。
- 跨平台一致性:确保移动端与桌面端体验统一,适配多样化设备。
这段代码主要用于监控前端应用中的网络请求(XMLHttpRequest 和 fetch),会收集请求的性能指标及
错误信息进行上报。以下是具体分析:
1. resetXhr() - 监控 XMLHttpRequest
- 重写 XMLHttpRequest:通过修改原型方法
open
和send
,拦截所有 XHR 请求。 - 关键行为:
- 记录请求 URL 和方法(GET/POST 等)。
- 过滤监控接口自身请求(
url.indexOf(monitorUrl)
),避免循环上报。 - 计算请求耗时(从
loadstart
到readystatechange
完成)。 - 根据状态码和响应时间判定请求状态:
status < 200 || status >= 300 // HTTP 错误状态码 || (status==200 && response.code !=200) // 业务层错误码 || cost >= 3000 // 慢请求(超过3秒)
- 上报数据包含:请求路径、耗时、错误信息等。
2. resetFetch() - 监控 Fetch API
- 重写 fetch 方法:通过包裹原生 fetch 实现监控。
- 监控维度:
- 记录请求的 URL、方法、请求头、请求体。
- 跟踪请求耗时(从发起请求到响应完成)。
- 根据响应状态(ok 属性)和网络错误判定请求状态。
- 无论成功/失败都会上报完整请求信息。
上报逻辑
通过 toReport()
方法将数据暂存 localStorage,达到阈值(默认5条)或立即上报模式时批量发送到服务端。
设计目的
- 性能监控:识别慢请求、接口成功率
- 错误追踪:捕捉 HTTP 错误、业务错误
- 网络诊断:记录完整请求/响应信息便于问题复现
注意:代码中
monitorUrl
需指向监控系统的接收接口,避免监控数据自身请求被重复上报。
以下是关于 XMLHttpRequest 中 open()
和 send()
方法的解释:
一、XMLHttpRequest 是什么
XMLHttpRequest 是浏览器提供的原生 API,用于在 JavaScript 中发起 HTTP 请求。它是实现 AJAX(异步通信)的核心对象。
二、open() 方法
xhr.open(method, url, async)
- 作用:初始化请求配置
- 参数说明:
method
:请求方法(GET/POST/PUT/DELETE 等)url
:请求地址async
(可选):是否异步请求,默认 true
- 在代码中的体现:
xmlhttp.prototype.open = function (...args) {this.url = args[1] // 记录请求地址this.method = args[0] // 记录请求方法return originOpen.apply(this, args); // 调用原生方法 };
三、send() 方法
xhr.send(body)
- 作用:正式发送请求
- 参数说明:
body
:请求体数据(GET 请求通常为 null,POST 请求可传 FormData/String 等)
- 在代码中的改造:
xmlhttp.prototype.send = function (...args) {// 添加性能监控逻辑xml.addEventListener('readystatechange', function() { /* 计算耗时 */ });xml.addEventListener('loadstart', function() { /* 记录开始时间 */ });return originSend.apply(this, args); // 调用原生方法 }
四、完整请求流程示例
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true); // 初始化
xhr.send(); // 发送请求
五、在监控代码中的作用
你的代码通过重写这两个方法:
- 记录请求元数据:在
open()
中捕获 URL 和请求方法 - 注入监控逻辑:在
send()
中:- 过滤监控接口自身请求(
monitorUrl
) - 计算请求耗时(从 loadstart 到 readyState 变化)
- 根据状态码和响应时间判定请求状态
- 过滤监控接口自身请求(
类比理解
可以把 open()
理解为填写快递单(指定收件地址和邮寄方式),send()
则是正式将包裹交给快递公司寄出的动作。监控代码相当于在快递站加装了摄像头(记录发货时间)和质检设备(检查包裹状态)。
不需要特别处理 axios,但需要明确以下关键点:
1. 拦截原理
你的代码通过 重写 XMLHttpRequest 原型方法 实现监控,而 axios 在浏览器环境中正是基于 XMLHttpRequest 实现的。因此:
// 所有使用原生 XHR 的库(包括 axios)都会被自动拦截
axios.get('/api/data') // ← 这个请求会被你的 resetXhr() 捕获
2. 例外情况
需注意以下两种特殊情况:
// 情况一:如果 axios 配置了 adapter 使用 fetch
axios.defaults.adapter = require('axios/lib/adapters/fetch');
// 此时需要启用你的 resetFetch() 方法(当前代码中该方法被注释)// 情况二:使用 nodejs 环境的 axios(非浏览器环境)
// 你的监控代码不会生效
3. 验证方式
可通过以下代码测试:
// 测试请求
axios.get('https://api.example.com/data').then(console.log)// 在 Chrome 控制台查看网络请求:
// 1. 该请求的 Initiator 列会显示 "xhr" 标识
// 2. 监控系统应能收到该请求的上报数据
4. 最佳实践建议
// 在项目入口文件初始化监控
import Monitor from '@/utils/monitor';
new Monitor({ cacheMax: 10 }); // ← 需在 axios 实例创建前执行// 后初始化 axios
import axios from 'axios';
const service = axios.create({ /* 配置 */ });
总结表格
条件 | 是否被监控 | 说明 |
---|---|---|
浏览器环境使用 axios | ✅ | 依赖 XMLHttpRequest |
使用 fetch 的 axios | ❌ | 需取消注释 resetFetch() |
Node.js 环境 | ❌ | 无 XMLHttpRequest 对象 |
当前代码中
resetFetch()
方法被注释,如需监控 fetch 请求需取消注释该方法。
以下是代码中拦截网络请求的关键步骤解析:
拦截发生的核心步骤
resetXhr = () => {const xmlhttp = window.XMLHttpRequest;const originOpen = xmlhttp.prototype.open; // ① 保存原生 open 方法const originSend = xmlhttp.prototype.send; // ② 保存原生 send 方法// ③ 重写 open 方法(记录请求元数据)xmlhttp.prototype.open = function (...args) {this.url = args[1]; // 记录请求地址this.method = args[0]; // 记录请求方法return originOpen.apply(this, args); // ④ 调用原生 open};// ⑤ 重写 send 方法(注入监控逻辑)xmlhttp.prototype.send = function (...args) {if (过滤监控接口) return;// ⑥ 添加事件监听(性能采集)xml.addEventListener('readystatechange', function() {// 当 readyState === 4 时计算耗时});xml.addEventListener('loadstart', function() {// 记录请求开始时间});return originSend.apply(this, args); // ⑦ 调用原生 send};
}
拦截触发时机流程图
浏览器发起请求↓
new XMLHttpRequest()↓
xhr.open() ← 被重写的方法(记录 URL/METHOD)↓
xhr.send() ← 被重写的方法(注入监控逻辑)├─▶ 执行原生 send↓
监控逻辑触发:├─ loadstart 事件 → 记录开始时间(精度高于 Date.now())└─ readystatechange 事件 → 计算耗时/状态码判定
对 axios 的影响说明
由于 axios 的浏览器实现基于 XMLHttpRequest,当以下条件满足时会被自动监控:
// 项目初始化顺序示例
// 1. 先初始化监控(必须!)
new Monitor(); // 2. 后创建 axios 实例
const axiosInstance = axios.create();
特殊注意事项
// 如果 axios 配置了自定义适配器(将不会走 XHR 流程)
axiosInstance.defaults.adapter = customAdapter; // ← 这种情况无法被监控// 解决方案:保持默认配置或手动适配监控逻辑