当前位置: 首页 > article >正文

vue埋点

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插件,用于实时监控指标。这部分可以补充说明除了核心指标外,还有辅助工具帮助开发者更好地优化。

总结起来,回答需要涵盖以下几点:

  1. Web Vitals的定义和核心指标(LCP、FID、CLS),参考网页1、2、3。
  2. 每个指标的具体含义和标准值,结合网页1、2、5的描述。
  3. web-vitals库的作用和实际应用,如集成到项目、与分析工具结合,参考网页3、4、5、6。
  4. 这些指标的重要性和影响,比如用户体验、SEO优化,引用网页2和4的内容。
  5. 可能的辅助工具或扩展,如Chrome插件,来自网页3、7的信息。

Web Vitals 是 Google 提出的用于量化网页用户体验的核心性能指标体系,旨在简化开发者对网站健康度的评估。它通过三个核心指标(Core Web Vitals)和辅助指标,帮助优化加载速度、交互响应性和视觉稳定性。以下是具体解析:

一、核心指标与作用

  1. LCP(最大内容渲染时间)
    衡量页面从加载开始到视口内最大内容元素(如图片、标题文本块)完成渲染的时间。良好标准为 ≤2.5秒。若超时,用户可能因等待过久而流失,需优化资源加载或服务器响应。

  2. FID(首次输入延迟)
    记录用户首次与页面交互(点击、输入等)到浏览器实际响应的延迟时间。理想值为 ≤100毫秒。高延迟会导致用户感知卡顿,需减少主线程阻塞或优化代码执行效率。

  3. CLS(累计布局偏移)
    评估页面加载期间意外布局偏移的累积分数(0-1分)。优秀标准为 ≤0.1。常见于未预设尺寸的图片或动态插入内容,需通过占位符或预留空间避免布局跳动。

二、辅助工具与实现

测量工具
Google 提供开源库 web-vitals,支持通过 JavaScript 或 React 等框架集成。开发者可调用 getCLS()getFID() 等函数直接获取指标数据。
示例代码:

import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS(console.log);  // 输出 CLS 数据

三、优化意义与影响

  1. 用户体验提升:直接关联用户对页面流畅度、稳定性的感知,降低跳出率。
  2. SEO 排名优化:Google 已将 Core Web Vitals 纳入搜索排名算法,优化后可提高网站搜索可见性。
  3. 跨平台一致性:确保移动端与桌面端体验统一,适配多样化设备。

这段代码主要用于监控前端应用中的网络请求(XMLHttpRequest 和 fetch),会收集请求的性能指标及
错误信息进行上报。以下是具体分析:

1. resetXhr() - 监控 XMLHttpRequest

  • 重写 XMLHttpRequest:通过修改原型方法 opensend,拦截所有 XHR 请求。
  • 关键行为
    • 记录请求 URL 和方法(GET/POST 等)。
    • 过滤监控接口自身请求(url.indexOf(monitorUrl)),避免循环上报。
    • 计算请求耗时(从 loadstartreadystatechange 完成)。
    • 根据状态码和响应时间判定请求状态:
      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)
  1. 作用:初始化请求配置
  2. 参数说明
    • method:请求方法(GET/POST/PUT/DELETE 等)
    • url:请求地址
    • async(可选):是否异步请求,默认 true
  3. 在代码中的体现
    xmlhttp.prototype.open = function (...args) {
        this.url = args[1]  // 记录请求地址
        this.method = args[0]  // 记录请求方法
        return originOpen.apply(this, args); // 调用原生方法
    };
    

三、send() 方法

xhr.send(body)
  1. 作用:正式发送请求
  2. 参数说明
    • body:请求体数据(GET 请求通常为 null,POST 请求可传 FormData/String 等)
  3. 在代码中的改造
    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(); // 发送请求

五、在监控代码中的作用

你的代码通过重写这两个方法:

  1. 记录请求元数据:在 open() 中捕获 URL 和请求方法
  2. 注入监控逻辑:在 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; // ← 这种情况无法被监控

// 解决方案:保持默认配置或手动适配监控逻辑

http://www.kler.cn/a/587189.html

相关文章:

  • Python实现NOA星雀优化算法优化随机森林分类模型项目实战
  • 前端工程化之前端工程化详解 包管理工具
  • Haskell语言的二进制与编码
  • 基于隐私计算的数据共享与分析平台V1.0源代码说明文档
  • AtCoder AT_abc397_d [ABC397D] Cubes
  • leetcode hot100普通动态规划/基础DP
  • OpenHarmony-SELinux配置
  • R语言高效数据处理-自定义格式EXCEL数据输出
  • 洞悉C++内存结构:解锁深层优化潜力
  • redis主从搭建
  • 《鸿蒙系统下AI模型训练加速:时间成本的深度剖析与优化策略》
  • 【ElasticSearch】学习笔记
  • 如何让ai问答机器人通人性?
  • Day16:最小的k个数
  • craftjs的示例landing项目改成APP路由
  • 我与DeepSeek读《大型网站技术架构》- 总结
  • 【零基础入门unity游戏开发——unity3D篇】物理系统 —— 3D物理材质Physics Material
  • 解锁下一代AI应用:开源项目mcp-server-qdrant如何重塑向量数据库管理?
  • EasyCVR安防视频汇聚平台助力工业园区构建“感、存、知、用”一体化智能监管体系
  • 异步加载错误如何解决