相关内容
- 什么是异常
- JS 处理异常的方式
- 异常上报方式
- 异常监控上报常见问题
- GitHub 前端代码异常监控方案
什么是异常
JS 脚本错误
一般分为语法错误 和 运行时错误
// 语法错误
// 语法错误在开发阶段就会暴露,所以一般不需要对其处理
var error = 'error'; // 大写分号
// Uncaught SyntaxError: Invalid or unexpected toke
// 运行时错误
error // 未定义变量
// Uncaught ReferenceError: error is not defined
静态资源加载异常
<img src="notfount.jpg" alt="加载不存在图片" />
<!-- Failed to load resource: net::ERR_FILE_NOT_FOUND -->
Promise 异常
Promise.reject('wrong')
// Uncaught (in promise) wrong
// axios 返回 Promise,请求异常会抛出错误
axios({
method: 'GET',
url: 'http://xxx.com' // 无效的地址
})
// GET http://xxx.com/ net::ERR_CONNECTION_TIMED_OUT
// Uncaught (in promise) Error: Network Error
跨域异常 Script error.
当不同域的脚本发生错误,无法获取错误信息
// http://localhost:5000/error.js
throw Error('跨域脚本抛出的错误')
// http://localhost:5000/error.js
throw Error('本站脚本抛出的错误')
<!-- http://localhost:3000/index.html -->
<script>
window.onerror = function() {
console.log(arguments)
return true
}
</script>
<script src="http://localhost:3000/test.js"></script>
<script src="http://localhost:5000/test.js"></script>
报错:
JS 异常处理
- JS 代码块作为一个任务压入任务队列中,JS 线程不断从队列中提取任务执行。
- 当任务执行过程中出现异常,且异常没有捕获处理,则会沿着调用栈一层层向外抛出,最终终止当前任务的执行。
- JS 线程会继续从任务队列中提取下一个任务继续执行。
<script>
error
console.log('永远不会执行')
</script>
<script>
console.log('我继续执行')
</script>
对异常进行上报,首先程序要能感知或捕获到异常的发生并进行处理。
try catch
try catch 是代码中常用的捕获脚本错误的方式,当 try 包装的代码块报错时,catch 将捕捉到错误的信息,页面也可以继续执行,不会被阻塞。
不过 try catch 只能捕获到 同步的运行时错误,对 语法错误 和 异步错误 无能为力。
语法错误 在编辑器中开发时会直接抛出,在项目上线前就可以发现。
而 异步错误 不容易发现,需要特别注意。
try {
setTimeout(() => {
error
})
} catch (e) {
console.log('无法感知异步错误')
}
总结:
- try catch 只能捕获 JS 脚本 同步的运行时错误
- 无法捕获 语法错误 和 异步错误
window.onerror
window.onerror 可以捕获 同步和异步的运行时错误。
当 JS 运行时错误发生时, window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()
。
可以重新自定义这个方法,对异常进行处理。
/**
* @param {String} msg 错误信息
* @param {String} url 出错文件
* @param {Number} row 行号
* @param {Number} col 列号
* @param {Object} error 错误详细信息
*/
window.onerror = function (msg, url, row, col, error) {
console.log({
msg, url, row, col, error
})
// 返回 true,可以阻止异常向上抛出
return true
};
setTimeout(() => {
error
})
不过,window.onerror 无法捕获 语法错误 和 网络请求异常错误。
语法错误 开发阶段容易察觉,所以不用特别处理。
<script>
// 先定义好 onerror
window.onerror = function(msg, url, row, col, err) {
// ...
}
// 接口请求异常 无法捕获
axios({
method: 'GET',
url: 'http://localhost:3000'
})
</script>
<!-- 静态资源请求异常 无法捕获 -->
<!-- window.onerror 是 JS 脚本发生错误时触发的事件执行函数,所以静态资源请求异常无法触发该事件。 -->
<img src="notfount.jpg" />
总结:
在实际使用过程中,onerror 主要用来捕获预料之外的错误,而 try catch 用来在可预见情况下监控特定的错误,两者结合使用更加高效。
- onerror 是 JS 脚本的全局捕获方法,最好写在所有 JS 脚本的前面,避免无法被捕获。
- onerror 无法捕获网络异常的错误。
- 静态资源
- 这是由于加载资源的元素触发 error 事件不会冒泡到 window,所以需要在捕获 阶段捕捉才行。
- 接口请求
net::ERR_CONNECTION_REFUSED
- 静态资源
- onerror 无法捕获 Promise 错误
- 例如 axios 请求接口异常后,返回的Promise会抛出错误
- Promise 错误可以被 Promise catch 捕获
window.addEventListener
当加载一个资源失败时,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror 处理函数。
这些 error 事件不会向上冒泡到 window,因此必须在捕获阶段将其捕捉,可以通过 window.addEventListener 设置事件传播模式 useCapture
,在捕获阶段捕捉错误。
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<img src="notfount.jpg" />
总结:
- 可以通过 window.addEventListener 在事件捕获阶段捕捉 静态资源请求异常
- 不过这种方式无法判断 HTTP 的状态,所以还需要配合服务端日志进行排查分析才行。
- 不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。
- 需要注意避免 addEventListener 重复监听。
Promise - catch & unhandledrejection
Promise 可以使用 catch 方便的捕获它抛出的异常。
但是如果没有写 catch,以上方法都无法捕获到错误,类似 try catch,Promise catch 也相当于在可遇见情况下监视错误。
new Promise(() => {
throw new Error('throw 抛出错误相当于调用 reject')
}).catch(console.log)
Promise.reject('reject 抛出错误').catch(console.log)
axios 请求会返回一个 Promise,发送的网络请求发生异常后,Promise 会抛出错误,这时可以用 catch 捕获。
项目中可能会用到很多 Promise 实例或基于 Promise 的库(axios),为了避免漏掉 Promise 异常,可以全局注册 unhandledrejection 事件监听 未被 catch 的Promise 异常。
MDN:
当
Promise
被 reject 且没有 reject 处理器的时候,会触发unhandledrejection
事件;这可能发生在window
下,但也可能发生在Worker
中。 这对于调试回退错误处理非常有用。
// addEventListener
window.addEventListener("unhandledrejection", event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
// onunhandledrejection
window.onunhandledrejection = event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
};
window.addEventListener('unhandledrejection', function (event) {
// ...您的代码可以处理未处理的拒绝...
// 防止默认处理(例如将错误输出到控制台)
event.preventDefault();
});
Script error.
线上的项目一般会做 CDN 优化,这就会导致访问的页面和脚本来自不同的域名。
如果没有进行相应的配置,当访问的脚本报错,就会产生 Script error.
。
Script error.
是浏览器在同源策略限制下产生的。
浏览器出于安全的考虑,当页面引用非同域的外部脚本文件中抛出异常的话。
当前页面是没有权力知道这个报错信息的,目的是避免数据泄露到不安全的域中。
取而代之的就是输出 Script error.
这样的信息。
解决办法:
- 将脚本放到同域下,这样舍弃了 CDN 的优势
- 开启 CORS(跨源资源共享机制),从根本上解决,需要后端配置
CORS(跨源资源共享机制)
首先为页面的 script 标签添加 crossOrigin 属性。
<!-- http://localhost:3000/index.html -->
<script>
window.onerror = function() {
console.log(arguments)
return true
}
</script>
<script src="http://localhost:5000/test.js" crossorigin></script>
// http://localhost:5000/error.js
throw Error('跨域脚本抛出的错误')
其次需要后端为 http://localhost:5000
配置 Access-Control-Allow-Origin
响应头。
以 serve
开启的web服务为例:
# --cors 命令启用 CORS 并配置 `Access-Control-Allow-Origin` 为 `*`
serve . --cors
Vue & React 错误捕获
异常上报方式
监控拿到报错信息后,就需要将捕捉的错误信息发送到信息收集平台上,常用的发送形式有两种:
- 通过 Ajax 发送数据
Ajax 请求本身也有可能会发生异常,可能引发跨域问题,所以更推荐使用动态创建 img 标签的形式进行上报。
- 动态创建按 img 标签
function report (error) {
let reportUrl = 'http://xxx/report'
new Image().src = reportUrl + '?error=' + error
}
异常监控上报常见问题
压缩代码无法定位到错误的具体位置
现在大多数项目都是通过 webpack 等工具打包压缩后发布到线上的。
多个文件压缩打包成一个文件,变量名被替换成了简短的未知字符串,代码被合并成了一行。
想要定位错误在源代码中的位置,就要使用 Source Map。
异常信息量太大
如果网站访问量很大,错误采集就会有两个问题:
- 上报请求次数庞大
- 错误日志记录太多
采集率
如果没有必要将所有错误信息全部采集下来,可以设置一个采集率,减少采集的信息量。
采集率可以通过使用任意方式设定,随机数、时间、用户特征等。
function report (error) {
// 使用一个随机数
if (Math.random() < 0.3) {
let reportUrl = 'http://xxx/report'
new Image().src = reportUrl + '?error=' + error
}
}
本地存储
如果不需要及时记录错误,可以先将错误日志存储到客户端,定时执行上报操作。
例如:
- 发生异常,捕获错误信息,存储到 localStorage
- 对比当前时间和上次上报的时间
- 如果大于10分钟,就执行上报,并清空本地存储的日志
- 如果小于10分钟,就不作处理
当然,这种方式可能也会丢失一些记录,例如某个客户端在上报前退出,并再也不访问网站。
总结
- 在可预见的地方增加 try catch 监控
- 全局监控 JS 异常 window.onerror
- 全局监控静态资源异常 window.addEventListener
- 捕获没有 Catch 的 Promise 异常:unhandledrejection
- VUE errorHandler 和 React componentDidCatch
- 跨域 crossOrigin 解决
参考
GitHub 前端代码异常监控方案
GitHub 搜索关键字 前端异常
或 monitor
,筛选 JavaScript
可以查到一些开源的监控方案,例如: