前端性能监控揭秘

2,420 阅读9分钟

最近参考了大量前端性能监控的资料、项目,自己也搞了一套前端性能监控系统,有兴趣的话,可以了解下我的项目源码:
github: github.com/zxhnext/web…
npm: www.npmjs.com/package/web…

以下是对前端性能监控的部分见解以及部分项目源码,欢迎一起讨论、交流。

1、页面性能监控

1.1 页面加载过程

页面的性能怎么衡量?首先我们需要知道一个页面从开始输入网址回车到完全呈现中间都干了什么。下面是一张完整的图,来分析一下: 页面加载过程 如上图所示,我们来看看每一部分分别都做了什么:

+-- prompt for unload: 卸载旧页面,请求新页面
    +-- navigationStart: 开始记时,开始处理请求页面的起始时间
+-- redirect: 定向url
+-- unload: 继续卸载旧页面
+-- Appcache: 检查是否有离线缓存
# 以上操作全是在本地完成的,下面一步才开始连接网络
+-- DNS
    +-- domainLookupStart: 开始域查找
    +-- domainLookupEnd: IP地址翻译结束
+--  TCP:开启TCP连接,在请求资源时,是复用一个tcp连接的,以进行js,css等的串行下载
+-- response 响应
    +-- responseEnd: 响应完成,开始处理数据,需要注意的是,在响应过程中是不处理数据的,响应完成后才开始处理数据
+-- Processing: 浏览器开始工作
    +-- domLoading: 将dom载入到内存当中
    +-- domInteractive: 解析dom,创建dom树
    +-- domInteractive~domContentLoaded: 这个过程中继续拿取css、js、图片资源等,到domContentLoaded时dom内容全部处理完成
    +-- domContentLoaded~domComplete: 这个过程中做一些其它事情,如提取页面信息,设置缓存参数等,到domComplete时dom处理结束
# 我们需要知道的是,从prompt for unload一直到Processing页面一直是白的。
+-- onLoad: html标签中的onLoad,开始执行脚本,处理函数等

从上我们可以得出页面呈现过程中的几个关键节点,来看下图:

TTFB:Time To First Byte, ⾸字节时间
FP:First Paint, ⾸次绘制,绘制body
FCP:First Contentful Paint,首次有内容的绘制,第一个dom元素绘制完成
FMP:First Meaningful Paint,⾸次有意义的绘制
TTI:Time To Interactive, 可交互时间。整个内容渲染完成

我们截取一个页面的加载过程,来比对一下:

1.2 longtask

简单来说,任何在浏览器中执行超过 50 ms 的任务,都是 long task。
来看下图: longtask long task 会长时间占据主线程资源,进而阻碍了其他关键任务的执行/响应,造成页面卡顿。

1.3 Performance

Performance 接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing APIUser Timing APIResource Timing API

该类型的对象可以通过调用只读属性 Window.performance 来获得。

1.3.1 PerformanceNavigationTiming

下面我们通过PerformanceNavigationTiming来获取1.1中页面各个加载时间段:

 const {
    timing
} = window.performance
let times = {}
const loadTime = timing.loadEventEnd - timing.loadEventStart
if (loadTime < 0) {
    setTimeout(() => {
        getTiming()
    }, 200)
    return
}

// 网络建立连接
// 上一个页面卸载总耗时
times.prevPage = timing.fetchStart - timing.navigationStart
// 上一个页面卸载
times.prevUnload = timing.unloadEventEnd - timing.unloadEventStart
//【重要】重定向的时间
times.redirectTime = timing.redirectEnd - timing.redirectStart
// DNS 缓存时间
times.appcacheTime = timing.domainLookupStart - timing.fetchStart
//【重要】DNS 查询时间
//【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
times.dnsTime = timing.domainLookupEnd - timing.domainLookupStart
//【重要】读取页面第一个字节的时间
//【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
times.ttfbTime = timing.responseStart - timing.navigationStart
// tcp连接耗时
times.tcpTime = timing.connectEnd - timing.connectStart
// 网络总耗时
times.network = timing.connectEnd - timing.navigationStart

// 网络接收数据
// 前端从发送请求到接收请求的时间
times.send = timing.responseStart - timing.requestStart
// 接收数据用时
//【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
times.receive = timing.responseEnd - timing.responseStart
// 请求页面总耗时
times.request = timing.responseEnd - timing.requestStart

// 前端渲染
// 解析dom树耗时
times.analysisTime = timing.domComplete - timing.domInteractive // timing.domLoading
//【重要】执行 onload 回调函数的时间
//【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
times.onload = timing.loadEventEnd - timing.loadEventStart
// 前端总时间
times.frontend = timing.loadEventEnd - timing.domLoading

// 白屏时间
times.blankTime = timing.domLoading - timing.navigationStart
// domReadyTime(dom准备时间)
times.domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart
//【重要】页面加载完成的时间
//【原因】这几乎代表了用户等待页面可用的时间
times.loadPage = timing.loadEventEnd - timing.navigationStart
// 可操作时间
times.domIteractive = timing.domInteractive - timing.navigationStart

1.3.2 PerformanceEntry

来获取1.2中的FP,FCP,监听paint,此时返回一个列表,分别包含了FP、FCP。
返回参数中startTime为启动时间,duration为执行时间。

const observerPaint = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        ttiTime[entry.name] = entry.startTime + entry.duration
    }
})

observerPaint.observe({
    entryTypes: ['paint']
})

用同样的方法,我们来获取longtask

const observerLongTask = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        ttiTime[entry.name] = entry.startTime + entry.duration
    }
})

observerLongTask.observe({
    entryTypes: ['longtask']
})

longtask建议放在空闲时再执行,此时我们可以使用requestIdleCallback和web Worker.

1.3.3 tti-polyfill

最后,我们通过tti-polyfill包来获取页面tti,来看怎样使用

import ttiPolyfill from 'tti-polyfill'
ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
    console.log(tti)
})

1.3.4 performance.now

此时,我们的页面性能数据已经都可以获取到了,接下来,我们怎样获取一个函数的运行时长呢,这里我找到两种方式。一是用performance.now(),来看如何操作:

async function measure(fn) {
    const t0 = performance.now()
    await fn()
    const t1 = performance.now()
    return t1 - t0
}

function fn() {
    for(let i=0; i<100000; i++) {}
}
measure(fn)

1.3.5 performance.mark()

来看第二种,我们通过performance.mark()来获取

const prefix = fix => input => `${fix}${input}`
const prefixStart = prefix('start')
const prefixEnd = prefix('end')

const measure = async (fn, name = fn.name) => {
    const startName = prefixStart(name)
    const endName = prefixEnd(name)
    performance.mark(startName)
    await fn()
    performance.mark(endName)
    // 调用 measure
    performance.measure(name, startName, endName)
    const [{
        duration
    }] = entries
    performance.clearMarks(startName)
    performance.clearMarks(endName)
    performance.clearMeasures(name)
    return duration
}

// 使用时
function foo() {
  // some code
  for (let i = 0; i < 10000; i++) {
    // console.log(i)
  }
}
measure(foo)

此外,我们还可以通过performance.mark()获取css,文本等加载时间,来试一下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>页面渲染性能优化</title>
    <style>
        body {
            background: gray;
        }
    </style>
    <link href="https://cdn.bootcss.com/animate.css/3.7.0/animate.min.css" rel="stylesheet">
    <script>
        performance.mark("css done");
    </script>
</head>

<body>
    <div id="app">
        111
        <script>
            performance.mark("text done");
        </script>

    </div>
    <!-- <script src="./demo/1.fp&FTP.js"></script> -->
    <script>
        const perfEntries = performance.getEntriesByType("mark");
        for (const entry of perfEntries) {
            console.log(entry);
            console.log(entry.name + "执行时间", entry.startTime + entry.duration);
        }
    </script>
</body>
</html>

1.4 如何捕获SPA应用页面渲染时间

初步想法:在vue的beforeCreated中用preformance.now()计时,并在mounted中计时

2、页面错误捕获

2.1 JS错误捕获

我们来看看js捕获错误的几种方法,并分析其优缺点:

2.1.1 脚本独立执行

两个脚本独立执行,彼此是不影响的

<script>
  error
  console.log(1)
</script>
<script>
  // 会执行,两种脚本时独立的
  console.log(2)
</script>

2.1.2 try-catch

try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误是捕捉不到的。

// 1. 普通错误,可以捕获到
try {
    error // 未定义变量 
} catch (e) {
    console.log(e);
}

// 2. 语法错误,捕获不到
try {
    var error = 'error'// 大写分号
} catch (e) {
    console.log(e);
}

// 3. 异步错误,捕获不到
try {
  setTimeout(() => {
    error // 异步错误
  })
} catch(e) {
  console.log(e);
}

2.1.3 window.onerror

  1. 无论是异步还是非异步错误,onerror 都能捕获到运行时错误,但语法错误onerror同样捕获不到。
  2. window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx
  3. window.onerror无法捕获404错误,如<img src="./404.png">,是捕获不到的
let data = {}
window.onerror = (msg, url, line, col, error) => {
    try {
        // 没有URL不上报!上报也不知道错误
        if (msg != "Script error." && !url) {
            return true
        }
        let key = msg.match(/(\w+)/g) || []
        data.name = key.length > 0 && key[0]
        data.type = key.length > 1 && key[1]
        data.msg = msg || null
        data.url = url || null
        data.line = line || null
        // 不一定所有浏览器都支持col参数
        // 不过 [IE]下 window.event 对象提供了 errorLine 和 errorCharacter,以此来对应相应的行列号信息
        data.col = col || (window.event && window.event.errorCharacter) || null

        if (!!error && !!error.stack) {
            // 如果浏览器有堆栈信息,直接使用
            data.stack = error.stack.toString()

        } else if (!!arguments.callee) {
            // 尝试通过callee拿堆栈信息
            var ext = []
            // arguments.callee指向arguments对象的拥有函数引用, caller指向调用它的函数
            var fn = arguments.callee.caller
            var floor = 3 // 这里只拿三层堆栈信息
            while (fn && (--floor > 0)) {
                ext.push(fn.toString())
                //如果有环
                if (fn === fn.caller) {
                    break
                }
                fn = fn.caller
            }
            ext = ext.join(',')
            data.stack = ext
        }
        new Monitor(this.params, this.newCaptureClick).recordError(data)
    } catch (err) {
        console.log('js错误异常:', err)
    }
    return true
}

2.1.4 跨域错误

如果想监听不同域的js错误,需要两个步骤:

  1. 服务端相关的js文件上加上
Access-Control-Allow-Origin: * // response header
  1. 客户端引用相关的js文件时加上crossorigin属性
<script src="..." crossorigin="anonymous"></script>

2.1.5 扩展

如何捕获到页面中try/catch中的错误。在生成AST阶段找到try/catch,并上报错误。

2.2 捕获资源加载错误

2.2.1 Error事件捕获

虽然网络请求异常不会事件冒泡,但是在捕获阶段是可以将其捕获到的。

let data = {}
window.addEventListener('error', event => {
    try {
        if (!event) {
            return
        }
        let target = event.target || event.srcElement
        var isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement
        if (!isElementTarget) {
            return // js error不再处理
        }
        data.url = target.baseURI
        data.resourceUrl = target.src || target.href
        data.msg = `加载 ${target.tagName}资源错误\r\nlink:${data.resourceUrl}`
        new Monitor(this.params).recordError(data)
    } catch (error) {
        console.log('资源加载收集异常:', error)
    }
}, true)

2.2.2 performance.getEntries

依然通过performance.getEntries来获取资源加载

if (!window.performance || !window.performance.getEntries) {
    console.log('该浏览器不支持performance.getEntries方法')
    return
}
let entryTimesList = []
let entryList = window.performance.getEntries()
if (!entryList || entryList.length == 0) {
    return entryTimesList
}
entryList.forEach(item => {
    let templeObj = {}
    let usefulType = ['script', 'css', 'fetch', 'xmlhttprequest', 'link', 'img'] // 'navigation'
    if (usefulType.indexOf(item.initiatorType) > -1) {
        // 请求资源路径
        templeObj.name = item.name
        // 发起资源类型
        templeObj.initiatorType = item.initiatorType
        // http协议版本
        templeObj.nextHopProtocol = item.nextHopProtocol
        // dns查询耗时
        templeObj.dnsTime = item.domainLookupEnd - item.domainLookupStart
        // tcp链接耗时
        templeObj.tcpTime = item.connectEnd - item.connectStart
        // 请求时间
        templeObj.reqTime = item.responseEnd - item.responseStart
        // 重定向时间
        templeObj.redirectTime = item.redirectEnd - item.redirectStart
        entryTimesList.push(templeObj)
    }
})

2.2.3 object.onerror

2.3 捕获promise错误

我们通过监听unhandledrejection来获取promise错误。此外需要注意的是,如果我们在promise中使用了catch来捕获错误,那么unhandledrejection是监控不到的(自测)

let data = {}
window.addEventListener('unhandledrejection', event => {
    try {
        if (!event || !event.reason) {
            return
        }
        //判断当前被捕获的异常url,是否是异常处理url,防止死循环
        if (event.reason.config && event.reason.config.url) {
            data.url = event.reason.config.url
        }
        const error = event && event.reason
        const stack = error.stack || ''
        // Processing error
        let resourceUrl
        let errs = stack.match(/\(.+?\)/)
        if (errs && errs.length) {
            errs = errs[0]
            errs = errs.replace(/\w.+[js|html]/g, $1 => { data.resourceUrl = $1; return ''; })
            errs = errs.split(':')
            if (errs.length > 1) {
                data.line = parseInt(errs[1] || null)
                data.col = parseInt(errs[2] || null)
            }
        }
        data.msg = event.reason.message || event.reason
        data.responseTime = event.timeStamp // 响应时间
        data.url = event.target.document.URL
        new Monitor(this.params).recordError(data)
    } catch (error) {
        console.log('Promise错误监控', error)
    }
    return true
})

2.4 监控vue错误

通过vue自身的errorHandler来监控vue错误

Vue.config.errorHandler = (error, vm, info) => {
    try {
        let {
            message, // 异常信息
            name, // 异常名称
            script, // 异常脚本url
            line, // 异常行号
            column, // 异常列号
            stack // 异常堆栈信息
        } = error
        data.msg = message
        data.name = name
        data.stack = stack || null
        data.resourceUrl = script || null // 异常脚本url
        data.line = line || null // 异常行号
        data.col = column || null // 异常列号

        let errs = stack.match(/\(.+?\)/)
        if (errs && errs.length) {
            errs = errs[0]
            errs = errs.replace(/\w.+[js|html]/g, $1 => { data.resourceUrl = $1; return ''; })
            errs = errs.split(':')
            if (errs.length > 1) {
                data.line = parseInt(errs[1] || null)
                data.col = parseInt(errs[2] || null)
            }
        }
        data.vueInfo = info
        if (Object.prototype.toString.call(vm) === '[object Object]') {
            data.vueComponentName = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name
            data.vuePropsData = vm.$options.propsData
        }
        data.level = ErrorLevelEnum.WARN
        data.category = ErrorCategoryEnum.VUE_ERROR
        new Monitor(this.params).recordError(data)
    } catch (error) {
        console.log('vue错误异常:', error)
    }
}

2.5 监控iframe错误

for(let i = 0; i < window.frames.length; i++) {
    let data = {}
    window.frames[i].onerror = (msg, url, row, col, error) => {
        try {
            // 没有URL不上报!上报也不知道错误
            if (msg != "Script error." && !url) {
                return true
            }
            let key = msg.match(/(\w+)/g) || []
            data.name = key.length > 0 && key[0]
            data.type = key.length > 1 && key[1]
            data.msg = msg || null
            data.url = url || null
            data.line = row || null
            data.col = col || null
            if (!!error && !!error.stack) {
                // 如果浏览器有堆栈信息,直接使用
                data.stack = error.stack.toString()
            }
            new Monitor(this.params).recordError(data)
        } catch (err) {
            console.log('iframe错误异常:', err)
        }
        return true
    }
}

2.6 ajax错误监控

2.6.1 原生ajax错误监控

/**
 * 获取错误信息
 */
if (!window.XMLHttpRequest) {
    return
}
// 保存原生的 open 方法
let xhrOpen = XMLHttpRequest.prototype.open
// 保存原生的 send 方法
let xhrSend = XMLHttpRequest.prototype.send
let data = {
    request: {
        method: 'GET'
    }
}
let _handleEvent = (event, arg) => {
    try {
        if (event && event.currentTarget && event.currentTarget.status !== 200) {
            data.msg = `${data.request.method} ${event.target.responseURL} ${event.target.status} (${event.target.statusText})`
            data.responseTime = event.timeStamp
            data.request.params = JSON.parse(arg[0]) || {}
            data.request.url = event.target.responseURL
            data.response = {
                status: event.target.status,
                responseText: event.target.responseText
            }
            new Monitor(params).recordError(data)
        }
    } catch (error) {
        console.log('监听XHR错误:', error)
    }
}
// 重写 open
XMLHttpRequest.prototype.open = function() {
    // 先在此处取得请求的method
    data.request.method = arguments[0]
    // 再调用原生 open 实现重写
    return xhrOpen.apply(this, arguments)
}
// 重写 send
XMLHttpRequest.prototype.send = function () {
    if (this['addEventListener']) {
        this['addEventListener']('error', e => _handleEvent(e, arguments)) // 失败
        this['addEventListener']('load', e => _handleEvent(e, arguments)) // 完成
        this['addEventListener']('abort', e => _handleEvent(e, arguments)) // 取消
    } else {
        let tempStateChange = this['onreadystatechange']
        this['onreadystatechange'] = function (event) {
            tempStateChange.apply(this, arguments)
            if (this.readyState === 4) {
                _handleEvent(event, arguments)
            }
        }
    }
    // 再调用原生 send 实现重写
    return xhrSend.apply(this, arguments)
}

2.6.2 fetch监控

if(!window.fetch) return
let _oldFetch = window.fetch
let data = {
    request: {
        method: 'GET'
    }
}
let _handleEvent = () => {
    try {
        new Monitor(params).recordError(data)
    } catch (error) {
        console.log('监控fetch错误:', error)
    }
}

window.fetch = function () {
    const arg = arguments
    const args = Array.prototype.slice.apply(arg)
    if (!args || !args.length) return result
    if (args.length === 1) {
        if (typeof args[0] === 'string') {
            data.request.url = args[0]
        } else if (utils.isObject(args[0])) {
            data.request.url = args[0].url
            data.request.method = args[0].method || 'GET'
            data.request.params = JSON.parse(args[0].body) || {}
        }
    } else {
        data.request.url = args[0]
        data.request.method = args[1].method || 'GET'
        data.request.params = JSON.parse(args[1].body) || {}
    }
    return _oldFetch.apply(this, arguments)
    .then(res => {
        if (res.status !== 200) { // True if status is HTTP 2xx
            // 上报错误
            data.response = {
                status: res.status,
                responseText: res.statusText
            }
            data.msg = `${data.request.method} ${res.url} ${res.status} (${res.statusText})`
            _handleEvent()
        }
        return res
    })
    .catch(error => {
        // 上报错误
        data.msg = error.stack || error
        _handleEvent()
        throw error
    })
}

2.6.3 axios监控

let data = {
    response: {},
    request: {}
}
if (!window.axios) return;
const _axios = window.axios
const List = ['axios', 'request', 'get', 'delete', 'head', 'options', 'put', 'post', 'patch']
List.forEach(item => {
    _reseat(item)
})

function _reseat(item) {
    let _key = null;
    if (item === 'axios') {
        window['axios'] = resetFn;
        _key = _axios
    } else if (item === 'request') {
        window['axios']['request'] = resetFn;
        _key = _axios['request'];
    } else {
        window['axios'][item] = resetFn;
        _key = _axios[item];
    }

    function resetFn() {
        const result = ajaxArg(arguments, item)
        return _key.apply(this, arguments)
            .then(function (res) {
                if (result.report === 'report-data') return res;
                try {
                    data.request.url = res.request.responseURL ? res.request.responseURL.split('?')[0] : '';
                    // data.request.responseText = res.request.responseText;
                    data.request.method = result.method
                    data.request.params = result.options
                } catch (e) {}
                return res
            })
            .catch((err) => {
                if (result.report === 'report-data') return res;
                data.msg = err.message
                data.response.status = err.response ? err.response.status : 0
                return err
            })
    }
}

// Ajax arguments
function ajaxArg(arg, item) {
    let result = {
        method: 'GET',
        type: 'xmlhttprequest',
        report: ''
    }
    let args = Array.prototype.slice.apply(arg)
    try {
        if (item == 'axios' || item == 'request') {
            result.url = args[0].url
            result.method = args[0].method
            result.options = result.method.toLowerCase() == 'get' ? args[0].params : args[0].data
        } else {
            result.url = args[0]
            result.method = ''
            if (args[1]) {
                if (args[1].params) {
                    result.method = 'GET'
                    result.options = args[1].params;
                } else {
                    result.method = 'POST'
                    result.options = args[1];
                }
            }
        }
        result.report = args[0].report
    } catch (err) {}
    return result;
}

3. 错误上报方式

3.1 Image上报

我们为什么使用img来进行错误上报,主要又以下两点考虑:

  1. 不存在ajax跨域问题,可做跨源请求
  2. 很古老的标签,没有浏览器兼容性问题
  3. img请求优先级比ajax低
  4. img是get请求,针对同样错误具有缓存特性,不会二次上报(别人说的,不清楚对不对) 使用方式:
try {
    var img = new Image()
    img.src = `${his.url}?v=${new Date().getTime()}&'${this.formatParams(data)}`
} catch (error) {
    console.log('发送消息出错:', error)
}

3.2 navigator.sendBeacon

navigator.sendBeacon可以在浏览器空闲时间发出请求,这样不会堵塞进程

navigator.sendBeacon(this.url, JSON.stringify(data))

4. 录屏

我们通过监听用户点击,使用html2Canvas来对用户操作进行捕获

// 异步引入js
const insertJs = (url = '') => {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script')
        script.type = 'text/javascript'
        script.src = url
        document.querySelector('head').appendChild(script)
        script.onload=function(){
            resolve()
        }
        script.onerror=function(){
            reject('js加载失败')
        }
    })
}

class CaptureClick{
    constructor(params = {}) {
        this.captureClick = params.captureClick || false // 是否录屏,只录制点击区域
        this.captureMode = params.captureMode || 1 // 截屏模式 1-最小区域 2 - 整屏,
        this.captureReportNum = params.captureReportNum || 1 // 截屏上报个数(最多10个)
        this.capturedDoms = []
    }
    
    // 初始化,监听用户操作
    initCaptureClick () {
        let _self = this
        if (!_self.captureClick) return
        window.addEventListener('click', e => {
            let pathTemp = Array.from(e.path)
            // html2canvas截取目标只能是document内的dom,所以需要移除window和document
            pathTemp.pop() // 移除window
            pathTemp.pop() // 移除document
            if (_self.capturedDoms.length >= 10) {
                _self.capturedDoms.pop() // 抛出最后一个
            }
            //录屏模式 1- 最小 2- 全屏
            const path = _self.captureMode === 1 ? pathTemp[0] : pathTemp[pathTemp.length - 1]
            _self.capturedDoms.unshift(path) // 插入最前面
        }, true) // 捕获模式
    }

    /**
     * 录屏上报
     * @param options {url: 上报链接 id: 错误id}
     */
    reportCaptureImage (options) {
        if (!this.captureClick) {
            return
        }
        // 上报录屏个数合法性检查
        this.captureReportNum = this.captureReportNum > 10 ? 10 : this.captureReportNum
        this.captureReportNum = this.captureReportNum <= 0 ? 1 : this.captureReportNum
        const tobeReport = this.capturedDoms.slice(0, this.captureReportNum) || []
        // 从cdn上动态插入
        if (window.html2canvas) {
            if (tobeReport.length) {
                this.dom2img(tobeReport, options)
            }
        } else {
            insertJs("//unpkg.com/html2canvas@1.0.0-alpha.12/dist/html2canvas.min.js").then(() => {
                if (tobeReport.length && window.html2canvas) {
                    this.dom2img(tobeReport, options)
                }
            }).catch(error => {
                console.log('录屏失败:', error)
            }) 
        }  
    }

    dom2img (doms = [], options) {
        // 压缩图片地址
        let compressedUrlList = []
        if (window.LZString && LZString.compress) {
            doms.forEach((dom, index) => {
                html2canvas(dom).then(canvas => {
                    let imageUrl = canvas.toDataURL("image/png")
                    compressedUrlList[index] = LZString.compress(imageUrl) 
                    console.log('截屏压缩图片文件地址:', compressedUrlList[index])
                })
            })
            // new API(options.url).report({compressedUrlList, getErrorId: options.getErrorId})
        } else {
            insertJs('//unpkg.com/lz-string@1.4.4/libs/lz-string.js').then(() => {
                doms.forEach((dom, index) => {
                    html2canvas(dom).then(canvas => {
                        let imageUrl = canvas.toDataURL("image/png")
                        compressedUrlList[index] = LZString.compress(imageUrl) 
                        console.log('截屏压缩图片文件地址:', compressedUrlList[index])
                    })
                })
                // new API(options.url).report({compressedUrlList, getErrorId: options.getErrorId})
            }).catch(error => {
                console.log('压缩图片失败:', error)
            })
        }
    }
}

5. 参考项目

前端监控系统:github.com/DuLinRain/F…
前端监控系统:github.com/Jameszws/mo…

6. 下一步探究方向

接下来将继续研究以下几个方向

  1. 代码覆盖率
    参考资料: babel-plugin-istanbul
  2. 代码重复率
    参考资料: jscpd
  3. 前端打点