异常捕获问题
1. 异常捕获扩展到流行框架带来的问题?
1.1 Script error
我们合乎情理地在本地页面进行尝试捕获异常,如:
<!-- http://localhost:3031/ -->
<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>
[!NOTE] 一般的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是所有域都可以访问。
1.2 sourceMap
[!NOTE] 解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。
我们用webpack将代码打包压缩成bundle.js:
// webpack.config.js
var path = require('path');
// webpack 4.1.1
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:{}}...;
//# sourceMappingURL=bundle.js.map
意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:
version: 3, // Source map的版本
sources: ["webpack:///webpack/bootstrap", ...], // 转换前的文件
names: ["installedModules", "__webpack_require__", ...], // 转换前的所有变量名和属性名
mappings: "aACA,IAAAA,KAGA,SAAAC...", // 记录位置信息的字符串
file: "bundle.js", // 转换后的文件名
sourcesContent: ["// The module cache var installedModules = {};..."], // 源代码
sourceRoot: "" // 转换前的文件所在的目录
1.3 MVVM框架
现在越来越多的项目开始使用前端框架,在MVVM框架中如果你一如既往的想使用window.onerror来捕获异常,那么很可能会竹篮打水一场空,或许根本捕获不到,因为你的异常信息被框架自身的异常机制捕获了。
比如Vue 2.x中我们应该这样捕获全局异常:
Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 异常信息
name, // 异常名称
script, // 异常脚本url
line, // 异常行号
column, // 异常列号
stack // 异常堆栈信息
} = err;
// vm为抛出异常的 Vue 实例
// info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
目前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;
}
}
然后我们就可以这样使用该组件:
1.4 异常上报
1.4.1 sourceMap解析
[!NOTE] 其实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);
// 定义post接口
router.post('/errorMsg/', function(req, res) {
let error = req.body; // 获取前端传过来的报错对象
let url = error.scriptURI; // 压缩文件路径
if (url) {
let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // map文件路径
// 解析sourceMap
let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' + fileUrl), 'utf8')); // 返回一个promise对象
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将需要的异常信息统一传递给后台存储,完成异常上报。