这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
前言
上一篇讲了搭建前端监控系统的接口监控,因为本系列文章主要是讲关于前端收集数据的SDK的实现,这一篇来讲错误监控,收集错误的过程也能学习到很多东西。我们对于经过打包处理的前端应用还会使用sourcemap映射处理错误堆栈信息。
JavaScirpt的常见错误类型
-
SyntaxError:语法错误
-
Uncaught ReferenceError(引用错误)
-
RangeError(范围错误)
-
TypeError(类型错误)
-
URIError(URL 错误)
-
资源加载错误
-
接口错误
-
promise未处catch的错误
-
跨域脚本错误
我们来区分一下这些错误。
语法错误:
<script>
const value = 10
@!
console.log(value)
</script>
js引擎在解析scrpit代码时,会先进行词法分析, 将js转换成[{}] 格式的tokens流, 为什么要这么做呢? 因为后面要将tokens流转换成抽象语法树, 在生成语法树的过程中,引擎会有语法分析器对语法进行判断, 上述代码语法分析器判断无法生成一颗有效的语法树,抛出语法错误。停止对代码的解析。
抛出错误: Uncaught SyntaxError: Invalid or unexpected token
引用错误:
<script>
const value = 10
console.log(test)
console.log(value)
</script>
js引擎对当前script中的代码先进行词法分析,语法解析完成后,成功构成一颗ast语法树,此时js引擎会对当前ast树进行预编译。即在内存中开辟空间,将变量/函数存放到分配的空间中,声明变量/函数,此时变量/函数提升就发生在这个阶段。
预编译阶段的执行流程
- 创建GO对象
- 声明的变量赋予GO对象,值为undefined,声明的函数赋予GO对象,值为函数体
- 遇到函数,就会创建AO对象
- 查找函数形参及函数内变量声明,形参名及变量名作为AO对象的属性,值为undefined
- 实参形参相统一,实参值赋给形参
- 查找函数声明,函数名作为AO对象的属性,值为函数引用
- 一切声明的全局变量,全挂载到window上。
当预编译完成后,js引擎会进入运行阶段, 代码运行到console.log(test) 时,进行作用域查找,当查找到顶层window时此时还没有这个变量的声明,那么js引擎抛出错误. 代码不再向下执行。
抛出的错误test.html:12 Uncaught ReferenceError: test is not defined
范围错误:
<script>
const value = []
value.length = -1
console.log(value)
</script>
当预编译完成后,js引擎会进入运行阶段,代码运行到value.length = -1 时, js引擎发现了value的length被赋值为了-1。此时js引擎抛出 Uncaught RangeError: Invalid array length 。因为-1 值不在数组所允许的范围或者集合中。抛出错误后,js代码终止。
还有其他这几种情况
// Number 对象的方法参数超出范围
const num = new Number(12.34);
console.log(num.toFixed(-1));
// 函数堆栈超过最大值
const foo = () => foo();
foo(); // RangeError: Maximum call stack size exceeded
抛出的错误: Uncaught RangeError: Invalid array length
类型错误:
<script>
const value = []
const test = {};
test.go();
console.log(value)
</script>
值的类型或参数不是预期类型时发生的错误
抛出错误: Uncaught TypeError: test.go is not a function
URL 错误:
<script>
const value = []
decodeURI("%"); // URIError: URI malformed
console.log(value)
</script>
使用全局 URI 处理函数而产生的错误。
抛出错误: Uncaught URIError: URI malformed at decodeURI (<anonymous>)
资源加载错误:
<script src="./notfound.js"></script>
资源加载错误,即网站中的资源如果加载失败则抛出资源加载错误
抛出错误 GET http://127.0.0.1:5500/notfound.js net::ERR_ABORTED 404 (Not Found)
接口错误
axios.get('/notfound')
在我们上一章中对接口监控监听到这个错误
抛出错误: GET http://127.0.0.1:5501/notfound 404 (Not Found)
promise未catch的错误
<script>
const value = []
new Promise((resolve, reject) => {
resolve(a.b)
})
console.log(value)
</script>
在promise中出现的错误会被统一放到Promise的catch中进行处理。如果没有catch则向上抛出,因为执行机制的原因,并不会阻塞线程执行,所以value是可以正常打印
抛出错误: Uncaught (in promise) ReferenceError: a is not defined
跨域脚本错误
<script src="https://test.bootcdn.net/ajax/libs/test.js"></script>
前端需要在script标签中配置 crossorigin 才可以捕获到跨站脚本内部报出的错误,因为浏览器只允许同域下的脚本捕获具体的错误信息。
因为可能有些浏览器对crossorigin 不支持,此时我们用try catch继续向上抛出。
如何捕获
-
window.onerror
当发生 JavaScript 运行时错误(包括处理程序中引发的语法错误和异常)时,会触发window.onerror的回调。
但是window.onerror捕获不到资源加载错误
-
使用window.addEventListener('error')捕获资源错误,但是window.addEventListener('error')也可以捕获到js运行错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误。
既然window.addEventListener('error')也可以捕获到错误,那么我们为什么要用window.onerror呢?
因为window.onerror的事件对象数据更加多,更加清晰。
-
window.addEventListener('unhandledrejection')
捕获promise未catch的错误
为什么不用try/catch捕获
面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常
async await promise try...catch
设计错误监控数据结构
js运行加载时的数据结构
{
content: // 堆栈信息
col: // 列
row: // 行
message // 主要错误信息
name // 错误的主要name
resourceUrl // url
errorMessage // 完整的错误信息
scriptURI // 脚本url
lineNumber: // 行号
columnNumber // 列号
}
// 如果是使用webpack打包的框架代码报错,在处理一次
添加
{
source // 对应的资源
sourcesContentMap // sourceMap的信息
}
资源错误数据结构
{
url
}
Promise catch的错误
{
type: // 类型
reason // 原因
}
设计代码
- 对错误进行格式化处理
let formatError = errObj => {
let col = errObj.column || errObj.columnNumber // Safari Firefox 浏览器中才有的信息
let row = errObj.line || errObj.lineNumber // Safari Firefox 浏览器中才有的信息
let message = errObj.message
let name = errObj.name
let { stack } = errObj
if (stack) {
let matchUrl = stack.match(/https?:\/\/[^\n]+/) // 匹配从http?s 开始到出现换行符的信息, 这个不仅包括报错的文件信息而且还包括了报错的行列信息
let urlFirstStack = matchUrl ? matchUrl[0] : ''
// 获取到报错的文件
let regUrlCheck = /https?:\/\/(\S)*\.js/
let resourceUrl = ''
if (regUrlCheck.test(urlFirstStack)) {
resourceUrl = urlFirstStack.match(regUrlCheck)[0]
}
let stackCol = null // 获取statck中的列信息
let stackRow = null // 获取statck中的行信息
let posStack = urlFirstStack.match(/:(\d+):(\d+)/) // // :行:列
if (posStack && posStack.length >= 3) {
;[, stackCol, stackRow] = posStack
}
// TODO formatStack
return {
content: stack,
col: Number(col || stackCol),
row: Number(row || stackRow),
message,
name,
resourceUrl
}
}
return {
row,
col,
message,
name
}
}
- 对webpack打包的项目需要单独处理一次
let frameError = async errObj => {
const result = await $.get(`http://localhost:3000/sourcemap?col=${errObj.col}&row=${errObj.row}`)
return result
}
-
window.onerror
let _originOnerror = window.onerror window.onerror = async (...arg) => { let [errorMessage, scriptURI, lineNumber, columnNumber, errorObj] = arg let errorInfo = formatError(errorObj) // 如果是使用webpack打包的框架代码报错,在处理一次,这里暂时使用resourceUrl字段进行区分是否是框架代码报错 if (errorInfo.resourceUrl === 'http://localhost:3000/react-app/dist/main.bundle.js') { let frameResult = await frameError(errorInfo) errorInfo.col = frameResult.column errorInfo.row = frameResult.line errorInfo.name = frameResult.name errorInfo.source = frameResult.source errorInfo.sourcesContentMap = frameResult.sourcesContentMap } errorInfo._errorMessage = errorMessage errorInfo._scriptURI = scriptURI errorInfo._lineNumber = lineNumber errorInfo._columnNumber = columnNumber errorInfo.type = 'onerror' cb(errorInfo) _originOnerror && _originOnerror.apply(window, arg) } -
window.onunhandledrejection
let _originOnunhandledrejection = window.onunhandledrejection
window.onunhandledrejection = (...arg) => {
let e = arg[0]
let reason = e.reason
cb({
type: e.type || 'unhandledrejection',
reason
})
_originOnunhandledrejection && _originOnunhandledrejection.apply(window, arg)
}
- window.addEventListener
window.addEventListener(
'error',
event => {
// 过滤js error
let target = event.target || event.srcElement
let isElementTarget =
target instanceof HTMLScriptElement ||
target instanceof HTMLLinkElement ||
target instanceof HTMLImageElement
if (!isElementTarget) return false
// 上报资源地址
let url = target.src || target.href
cb({
url
})
},
true
)
参考文章
面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常
async await promise try...catch