您当前的位置:首页 > 计算机 > 编程开发 > Html+Div+Css(前端)

前端异常捕获与上报

时间:12-14来源:作者:点击数:

这次给大家分享下前端异常监控中需要了解的异常捕获与上报机制的一些要点,同时包含了实战性质的参考代码和流程。

首先,我们为什么要进行异常捕获和上报呢?

正所谓百密一疏,一个经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失,因此对于直面用户的前端而言,异常捕获与上报是至关重要的。

虽然目前市面上已经有一些非常完善的前端监控系统存在,如 sentry( sentry io/)、bugsnag( www.bugsnag 商业网/) 等,但是知己知彼,才能百战不殆,唯有了解原理,摸清逻辑,使用起来才能得心应手。

异常捕获方法

1. try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
  var a = 1;
  var b = a + c;
} catch (e) {
  // 捕获处理
  console.log(e); // ReferenceError: c is not defined
}

使用 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

我们合乎情理地在本地页面进行尝试捕获异常:

<!-- 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 配置为 *,意思是所有域都可以访问。

2. sourceMap

解决跨域或者将脚本存放在同域之后,你可能会将代码压缩一下再发布,这时候便出现了压缩后的代码无法找到原始报错位置的问题。如图,我们用 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 文件,那么我们该如何进行解析获取压缩前文件的异常信息呢?这个我会在下面异常上报的时候进行介绍。

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;
}
}

然后我们就可以这样使用该组件:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

详见官方文档:Error Handling in React 16( reactjs 组织网/blog/2017/07/26/error-handling-in-react-16.html)

异常上报

以上介绍了前端异常捕获的相关知识点,那么接下来我们既然成功捕获了异常,那么该如何上报呢?在脚本代码没有被压缩的情况下可以直接捕获后上传对应的异常信息,这里就不做介绍了,下面主要讲解常见的处理压缩文件上报的方法。

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); // 上报给node中间层处理
	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);
// 定义 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将需要的异常信息统一传递给后台存储,完成异常上报。

附:source-map API( www.npmjs 商业网/package/source-map)

3. 注意点

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。另外window.onerror这样的异常捕获不能捕获promise的异常错误信息,这点需要注意。

结语

前端异常捕获与上报是前端异常监控的前提,了解并做好了异常数据的收集和分析才能实现一个完善的错误响应和处理机制,最终达成数据可视化。本文详细实例代码地址: github /luozhihao/error-catch-report

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐