摘要
本文所实现的轻量级SDK主要包括错误监听和性能监控两部分,目前支持的性能指标为FCP、FP、LCP和TTFB。实现的大致思路为捕获相应的错误,监听相应的性能,然后进行数据组装处理,最后上报到指定的url。
错误监听
监听业务接口响应错误
所有的接口请求本质上都是XHR请求和fetch请求,所以当需要监听业务接口响应的错误时,可以重写xhr,fetch方法,对想要的数据进行捕获处理。
重写XHR,包括open和send方法
const XMLHttpRequest = window.XMLHttpRequest
// 缓存原始的open方法
const originOpen = XMLHttpRequest.prototype.open
// 重写open方法
XMLHttpRequest.prototype.open = function (
this: XMLHttpRequestFormat,
method: string,
url: string
) {
this.xhrData = {
method,
url,
sTime: Date.now(),
type: HTTPTYPE.XHR
}
return originOpen.apply(this, [method, url, true])
}
// 缓存原始的send方法
const originSend = XMLHttpRequest.prototype.send
// 重写send方法
XMLHttpRequest.prototype.send = function (
this: XMLHttpRequestFormat,
...args
) {
const { sTime, method, url } = this.xhrData
this.addEventListener('loadend', function (this: XMLHttpRequestFormat) {
// todo 判断返回状态是否发生错误,进行错误数据上报
})
return originSend.apply(this, args)
}
重写fetch方法
const originFetch = window.fetch
window.fetch = function (url, config) {
// todo 记录SDK上报所需数据
return originFetch.apply(window, [url, config]).then(
res => {
// todo 当返回的状态发生错误时进行数据上报
return res
},
(err: Error) => {
// todo 进行错误的数据上报
throw err
}
)
}
监听资源加载错误及js错误
资源加载的错误及js错误的监听通过window.addEventListener实现。
window.addEventListener(
'error',
function (event: ErrorEvent) {
// 资源加载错误
const errorTarget = event.target as ResourceErrorTarget
if (errorTarget.localName) {
// todo 进行资源加载错误数据组装与上报
} else {
// 捕获普通js错误
// todo 进行js错误数据组装与上报
}
},
true
)
监听开发中浏览器捕获到的未处理的Promise错误
一些浏览器能够捕获未处理的Promise错误,监听unhandledrejection事件,即可捕获到未处理的Promise错误。
window.addEventListener(
'unhandledrejection',
function (event: PromiseRejectionEvent) {
// todo 进行未处理的Promise错误数据组装与上报
},
true
)
监听vue错误
vue官方提供了一个API: app.config.errorHandler, 用于为应用内抛出的未捕获错误指定一个全局处理函数。
app.config.errorHandler = (err, instance, info) => {
// todo 进行vue错误数据组装与上报
console.error(err)
}
上报数据组装
捕获到相应的错误后,可针对错误种类编写相应的上报数据组装函数,此处偏定制化,一般根据个人或公司需求进行针对性的数据上报。这里比较复杂的处理为stack解析及sourcemap映射。
stack信息处理
在promise错误,js错误及vue错误的错误对象中都可以得到stack对象,stack的信息处理一般通过error-stack-parser包实现,从而消除各浏览器的差异,提取给定错误的原始文件名、行和列信息。
const stackFrames = ErrorStackParser.parse(error)
使用error-stack-parser库的时候,根据js的调用栈原理,只取stackFrames数组中的第一个元素即可定位到错误发生的文件。
sourcemap映射处理
现在的前端开发都是模块化、组件化的方式,在上线前对js和css文件进行合并压缩容易造成混淆,无法找到确切的报错文件与位置,sourcemap的作用就是将生成后的代码映射到源码文件中。 sourcemap的映射通过source-map-js包实现。
source-map-js包来源于source-map包,此处选择它的原因是source-map包中有一些前端无法支持的node语法,而source-map-js中对js进行了更好的支持。
sourcemap的还原流程
- 开启打包配置中的sourcemap选项(一般在打包配置文件中设置sourcemap为true),从而在打包后生成.map文件,将.map文件放到安全可靠的服务器位置,使用fetch请求获取.map文件内容。
- new 一个 sourceMapConsumer 的实例,表示一个已解析的源映射。
const consumer = new sourceMap.SourceMapConsumer() - 输入报错发生的行和列,可以得到源码对应的原始文件名、行和列信息。
consumer.originalPositionFor() - 从源文件的sourcesContent字段中,获取报错发生的源代码信息。
// 载入map文件
const rawSourceMapText = await this.loadSourceMap(fileName)
const rawSourceMap = JSON.parse(rawSourceMapText)
// 获取真实的报错行列数
const consumer = new sourceMap.SourceMapConsumer(rawSourceMap)
const position = consumer.originalPositionFor({
line: lineNumber,
column: columnNumber
})
const { source, line, column } = position
const { sources, sourcesContent } = rawSourceMap
// 找到报错信息源文件的源码信息
let index = sources.indexOf(source)
const lines = sourcesContent[index].split('\n')
性能监听
获取首屏加载时间(FCP)
这个指标用于记录浏览器从响应用户输入网址,到首屏内容渲染完成的时间。此时整个网页的内容不一定渲染完成,但当前视窗的内容需要渲染完毕。
function getFCP(): void {
if (isPerformanceObserverSupported()) {
const entryHandler = (list: PerformanceObserverEntryList) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
if (observer) {
observer.disconnect()
}
if (entry.startTime < getFirstHiddenTime().timeStamp) {
const value = Number(entry.startTime.toFixed(3))
// todo 组织value进行FCP上报
}
}
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({
type: 'paint',
buffered: true
})
}
}
获取白屏时间(FP)
记录浏览器从响应用户输入网址,到浏览器开始显示内容的时间。白屏时间是首屏时间的一个子集。
function getFP(): void {
if (isPerformanceObserverSupported()) {
const entryHandler = (list: PerformanceObserverEntryList) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-paint') {
if (observer) {
observer.disconnect()
}
if (entry.startTime < getFirstHiddenTime().timeStamp) {
const value = Number(entry.startTime.toFixed(3))
// todo 组织value进行FP上报
}
}
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({
type: 'paint',
buffered: true
})
}
}
获取最大内容绘制时间(LCP)
在大多数网页上,有一个元素因其大小和突出程度而与众不同。LCP是网站渲染包含最多内容的元素所花费的时间。网页通常是分阶段加载的,因此,页面上的最大元素也可能会发生变化。如果有任意一个新元素大于先前的最大元素内容,则浏览器还将报告一个新的PerformanceEntry。
当用户与页面进行交互(通过轻触、滚动或按键)时,浏览器将立刻停止报告新条目,因为用户交互通常会改变用户可见的内容。出于分析目的,应该仅报告最近一次分发的PerformanceEntry。
function getLCP(): void {
if (isPerformanceObserverSupported()) {
const entryHandler = (list: PerformanceObserverEntryList) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
if (lastEntry.startTime < getFirstHiddenTime().timeStamp) {
const value = Number(lastEntry.startTime.toFixed(3))
// todo 组织value进行LCP上报
}
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({
type: 'largest-contentful-paint',
buffered: true
})
// 若出现交互,中止LCP的计算
;['click', 'keydown'].forEach((event: string) => {
addEventListener(
event,
(): void => {
// 断开此观察者的连接
observer.disconnect()
},
{ once: true, capture: true }
)
})
}
}
获取首字节时间(TTFB)
首字节时间测量用户浏览器从服务器接收首个“字节”数据所需的时间,衡量服务器对访问者浏览器请求的响应能力。
function getTTFB(): void {
if (
isPerformanceObserverSupported() &&
PerformanceObserver.supportedEntryTypes?.includes('navigation')
) {
const entryHandler = (list: PerformanceObserverEntryList) => {
const [entry] = list.getEntriesByType('navigation')
if (observer) {
observer.disconnect()
}
const { requestStart, responseStart } =
entry as PerformanceNavigationTiming
const value = Number((responseStart - requestStart).toFixed(3))
// todo 组织value进行TTFB上报
}
const observer = new PerformanceObserver(entryHandler)
observer.observe({
type: 'navigation',
buffered: true
})
}
}
数据上报
目前采用XMLHttpRequest和image方法进行上报,后续可能迭代为sendBeacon方法上报。
imgRequest(data: ReportData, url: string): void {
let img: HTMLImageElement | null = new Image()
const spliceStr = url.indexOf('?') === -1 ? '?' : '&'
img.src = `${url}${spliceStr}data=${encodeURIComponent(
JSON.stringify(data)
)}`
img = null
}
async xhrPost(data: ReportData, url: string) {
const xhr = new XMLHttpRequest()
xhr.open('POST', url)
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.withCredentials = true
xhr.send(JSON.stringify(data))
}
总结
通过这四部分,一个简单的轻量级的SDK基本实现,后续会进行功能升级迭代,可能会再次更新一篇升级版文章,敬请期待。