Hello,你们好,又与你们见面了,此次给你们分享下前端异常监控中须要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。javascript
首先,咱们为何要进行异常捕获和上报呢?html
正所谓百密一疏,一个通过了大量测试及联调的项目在有些时候仍是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题惟有经过完善的监控机制才能有效的减小其带来的损失,所以对于直面用户的前端而言,异常捕获与上报是相当重要的。前端
虽然目前市面上已经有一些很是完善的前端监控系统存在,如sentry、bugsnag等,可是知己知彼,才能百战不殆,惟有了解原理,摸清逻辑,使用起来才能驾轻就熟。java
一般,为了判断一段代码中是否存在异常,咱们会这一写:node
try { var a = 1; var b = a + c; } catch (e) { // 捕获处理 console.log(e); // ReferenceError: c is not defined }
使用try catch可以很好的捕获异常并对应进行相应处理,不至于让页面挂掉,可是其存在一些弊端,好比须要在捕获异常的代码上进行包裹,会致使页面臃肿不堪,不适用于整个项目的异常捕获。react
相比try catch来讲window.onerror提供了全局监听异常的功能:webpack
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);
如图:git
window.onerror即提供了咱们错误的信息,还提供了错误行列号,能够精准的进行定位,如此彷佛正是咱们想要的,可是接下来即是填坑过程。github
咱们合乎情理地在本地页面进行尝试捕获异常,如:web
<!-- 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>
通常的CDN网站都会将Access-Control-Allow-Origin配置为*,意思是全部域均可以访问。
解决跨域或者将脚本存放在同域以后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码没法找到原始报错位置的问题。如图,咱们用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: "" // 转换前的文件所在的目录
若是你想详细了解关于sourceMap的知识,能够前往:JavaScript Source Map 详解
如此,既然咱们拿到了对应脚本的map文件,那么咱们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。
如今愈来愈多的项目开始使用前端框架,在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; } }
而后咱们就能够这样使用该组件:
<ErrorBoundary> <MyWidget /> </ErrorBoundary>
详见官方文档:Error Handling in React 16
以上介绍了前端异常捕获的相关知识点,那么接下来咱们既然成功捕获了异常,那么该如何上报呢?
在脚本代码没有被压缩的状况下能够直接捕获后上传对应的异常信息,这里就不作介绍了,下面主要讲解常见的处理压缩文件上报的方法。
当捕获到异常时,咱们能够将异常信息传递给接口,以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); // 上报给node中间层处理 xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头 xhr.send(JSON.stringify(errorObj)); // 发送参数 } }
其实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将须要的异常信息统一传递给后台存储,完成异常上报。下图能够看到控制台打印出了通过解析后的真是报错位置和文件:
以上是异常捕获和上报的主要知识点和流程,还有一些须要注意的地方,好比你的应用访问量很大,那么一个小异常均可能会把你的服务器搞挂,因此上报的时候能够进行信息过滤和采样等,设置一个调控开关,服务器也能够对类似的异常进行过滤,在一个时间段内不进行屡次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点须要注意。
最终大体的流程图以下:
前端异常捕获与上报是前端异常监控的前提,了解并作好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。本文详细实例代码地址:https://github.com/luozhihao/error-catch-report