本帖最后由 neekin 于 2018-3-29 15:47 编辑
Hello,大家好,又与大家见面了,这次给大家分享下前端异常监控中需要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。 首先,我们为什么要进行异常捕获和上报呢? 正所谓百密一疏,一个经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失,因此对于直面用户的前端而言,异常捕获与上报是至关重要的。 虽然目前市面上已经有一些非常完善的前端监控系统存在,如sentry、bugsnag等,但是知己知彼,才能百战不殆,唯有了解原理,摸清逻辑,使用起来才能得心应手。 异常捕获方法1. try catch通常,为了判断一段代码中是否存在异常,我们会这么写: try { var a = 1; var b = a + c; } catch (e) {
console.log(e);
}
使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。 2. window.onerror相比try catch来说window.onerror提供了全局监听异常的功能: window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { console.log('errorMessage: ' + errorMessage); console.log('scriptURI: ' + scriptURI); console.log('lineNo: ' + lineNo); console.log('columnNo: ' + columnNo); console.log('error: ' + error); };
console.log(a);
如图: window.onerror即提供了我们错误的信息,还提供了错误行列号,可以精准的进行定位,如此似乎正是我们想要的,但是接下来便是填坑过程。 异常捕获问题1. Script error.我们合乎情理地在本地页面进行尝试捕获异常,如: <script> window.onerror = function() { console.log(arguments); }; </script> <script src="http://cdn.xxx.com/index.js"></script>
这里我们把静态资源放到异域上进行优化加载,但是捕获的异常信息却是: 经过分析发现,跨域之后window.onerror是无法捕获异常信息的,所以统一返回Script error.,解决方案便是script属性配置 crossorigin=”anonymous” 并且服务器添加Access-Control-Allow-Origin。 `<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>`
一般的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是所有域都可以访问。 2. sourceMap解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。如图,我们用webpack将代码打包压缩成bundle.js: var path = require('path');
module.exports = {
mode: 'development', entry: './client/index.js', output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'client') }
}
最后我们页面引入的脚本文件是这样的: `!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;`
所以我们看到的异常信息是这样的: lineNo可能是一个非常小的数字,一般是1,而columnNo会是一个很大的数字,这里是730,因为所有代码都压缩到了一行。 那么该如何解决呢?聪明的童鞋可能已经猜到启用source-map了,没错,我们利用webpack打包压缩后生成一份对应脚本的map文件就能进行追踪了,在webpack中开启source-map功能: module.exports = { ...
devtool: '#source-map',
...
}
打包压缩的文件末尾会带上这样的注释: `!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}...;`
意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象: `version: 3, sources: ["webpack:///webpack/bootstrap", ...], names: ["installedModules", "__webpack_require__", ...], mappings: "aACA,IAAAA,KAGA,SAAAC...", file: "bundle.js", sourcesContent: ["// The module cache var installedModules = {};..."], sourceRoot: ""`
如果你想详细了解关于sourceMap的知识,可以前往:JavaScript Source Map 详解 如此,既然我们拿到了对应脚本的map文件,那么我们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。 3. MVVM框架现在越来越多的项目开始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror来捕获异常,那么很可能会竹篮打水一场空,或许根本捕获不到,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常: Vue.config.errorHandler = function(err, vm, info) { let { message, name, script, line, column, stack } = err;
}
目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。 同样的在react也提供了异常处理的方式,在 React 16.x 版本中引入了 Error Boundary: class ErrorBoundary extends React.Component { constructor(props) { super(props);
this.state = {
hasError: false }; } componentDidCatch(error, info) {
this.setState({
hasError: true }); logErrorToMyService(error, info); } render() {
if (this.state.hasError) {
return '出错了'; }
return this.props.children;
}
}
然后我们就可以这样使用该组件: <ErrorBoundary> <MyWidget />
</ErrorBoundary>
详见官方文档:Error Handling in React 16 异常上报以上介绍了前端异常捕获的相关知识点,那么接下来我们既然成功捕获了异常,那么该如何上报呢? 在脚本代码没有被压缩的情况下可以直接捕获后上传对应的异常信息,这里就不做介绍了,下面主要讲解常见的处理压缩文件上报的方法。 1. 提交异常当捕获到异常时,我们可以将异常信息传递给接口,以window.onerror为例: window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
var errorObj = {
errorMessage: errorMessage || null, scriptURI: scriptURI || null, lineNo: lineNo || null, columnNo: columnNo || null, stack: error && error.stack ? error.stack : null };
if (XMLHttpRequest) {
var xhr = new XMLHttpRequest();
xhr.open('post', '/middleware/errorMsg', true);
xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify(errorObj)); } } 2. sourceMap解析其实source-map格式的文件是一种数据类型,既然是数据类型那么肯定有解析它的办法,目前市面上也有专门解析它的相应工具包,在浏览器环境或者node环境下比较流行的是一款叫做’source-map’的插件。 通过require该插件,前端浏览器可以对map文件进行解析,但因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器,中间层代码如下: const express = require('express'); const fs = require('fs'); const router = express.Router(); const fetch = require('node-fetch'); const sourceMap = require('source-map'); const path = require('path'); const resolve = file => path.resolve(__dirname, file); router.post('/errorMsg/', function(req, res) { let error = req.body; let url = error.scriptURI;
if (url) {
let fileUrl = url.slice(url.indexOf('client/')) + '.map';
let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8'));
smc.then(function(result) {
let ret = result.originalPositionFor({
line: error.lineNo, column: error.columnNo });
let url = '';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
errorMessage: error.errorMessage, source: ret.source, line: ret.line, column: ret.column, stack: error.stack })
}).then(function(response) {
return response.json(); }).then(function(json) { res.json(json); }); }) } });
module.exports = router;
这里我们通过前端传过来的异常文件路径获取服务器端map文件地址,然后将压缩后的行列号传递给sourceMap返回的promise对象进行解析,通过originalPositionFor方法我们能获取到原始的报错行列号和文件地址,最后通过ajax将需要的异常信息统一传递给后台存储,完成异常上报。下图可以看到控制台打印出了经过解析后的真是报错位置和文件: 附:source-map API 3. 注意点以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点需要注意。 最终大致的流程图如下: 结语前端异常捕获与上报是前端异常监控的前提,了解并做好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。 |