# 异常监控

前端异常监控是指通过对前端代码的监控和分析,及时发现和定位前端代码中的异常,以便于及时修复和提高用户体验。

# try/catch

Javascript 代码中的 try/catch 块,可以用来捕获错误,并在错误处理程序中抛出错误。被捕获的错误不会影响后续代码执行,但是需要依赖开发人员手动对可能出现异常的代码块进行包裹。

try {
  // 执行可能会抛出错误的代码
  throw new Error("test");
} catch (error) {
  // 处理错误
  console.log(error); // Error: test at <anonymous>:3:9
}
成功
1
2
3
4
5
6
7

在try块中的代码如果抛出错误,那么程序会立即跳转到catch块中的代码。catch块中的error参数会包含错误的详细信息,包括错误类型和错误信息等。在catch块中,可以编写处理错误的逻辑,例如记录错误日志、提示用户等。

try/catch只能捕获当前执行的上下文抛出的错误。如果被包裹的代码块脱离了当前执行的上下文,则try/没法捕获。

try {
  // 执行可能会抛出错误的代码
  setTimeout(() => {
    throw new Error("test");
  }, 1000)
} catch (error) {
  // 处理错误
  console.log(error); // 不会被执行
}
成功
1
2
3
4
5
6
7
8
9

在实际开发中,开发人员只会针对一些特殊的部分使用try/catch。如果大量使用try/catch,那么不仅会增加开发负担,也会降低代码的可读性,因此需要一种对工程低侵入的错误采集方案。

# window.onerror

window.onerror 是一个用于 JavaScript 错误处理的全局事件。当 JavaScript 运行时出现未被捕获的错误时,window.onerror 会被触发,可以使用它来捕获和处理 JavaScript 运行时的异常。

window.onerror = function(message, source, lineno, colno, error) {
  // 处理异常信息
  console.log('错误信息', message);
  console.log('发生错误的脚本 URL', source);
  console.log('发生错误的行号', lineno);
  console.log('发生错误的列号', colno);
  console.log('错误Error对象的堆栈', error);
  // 返回true, 阻止执行默认事件处理函数,控制台不会在默认打印错误信息
  return true;
}
成功
1
2
3
4
5
6
7
8
9
10

window.onerror不能捕获语法错误和promise异步错误(包括网络错误、资源加载)。

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。

# window.addEventListener('error')

window.addEventListener('error') 具有window.onerror的大部分功能,但它并不能通过返回true的形式来阻止默认事件处理函数的执行,可以通过e.preventDefault()来阻止默认事件处理函数的执行。在错误的捕获时机上,它比onerror更早被触发。

window.addEventListener('error', function(event) {
  console.log('报错信息: ', event.message);
  console.log('错误文件地址: ', event.filename);
  console.log('错误行数: ', event.lineno);
  console.log('错误列数: ', event.colno);
  console.log('错误堆栈: ', event.error);
});
成功
1
2
3
4
5
6
7

资源加载的事件不会再window上冒泡,但是开发人员可以在事件捕获阶段采集错误,将addEventListener()的第三个参数设置为true,即可在事件捕获阶段采集到错误,从而实现对资源加载错误的监听。

window.addEventListener('error', function(event) {
  const { target } = e;
  console.log('资源加载失败标签:', target.nodeName);
  console.log('资源地址:', target.src);
}, true);
成功
1
2
3
4
5

开发人员在使用 window.addEventListener(error)采集错误时,需要区分 Javascript 错误和资源加载错误,可以利用 lineno 和 colno 字段实现。当资源加载失败时,回调参数中的两个字段的值为undefined。同时,也可以利用 instanceof 来判断,JavaScript 错误抛出的事件类型为 ErrorEvent,资源加载失败抛出的事件类型为 Event,ErrorEvent 为 Event 的子类型。优化后的代码如下:

window.addEventListener('error', (e) => {
  if (e instanceof ErrorEvent) {
    console.log('JavaScript 报错');
    console.log('报错信息:', message);
    console.log('错误文件地址:', filename);
    console.log('错误文件行数:', lineno);
    console.log('错误文件列数:', colno);
    console.log('错误堆栈:',error);
  } else {
    console.log('资源加载报错')
    console.log('资源加载失败标签:', target.nodeName);
    console.log('资源地址:', target,src);
  }
}, true);
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 异步错误

Javascript还有异步错误,这种错误没办法被上述三种方法捕获,需要使用addEventListener 监听 unhandledrejection 事件。

TIP

try/catch 能捕获到 async/await 并不是因为它本身的能力,而是因为async/await是语法糖,本身实现的时候,就捕获了异步错误,内部使用throw抛出

unhandledrejection 事件用于捕获异步操作中未被处理的 Promise 错误。当一个 Promise 被拒绝(rejected)但没有被处理(没有 catch 或者 then 带有第二个参数),这个事件就会被触发。

window.addEventListener('unhandledrejection', function(event) {
  console.warn('Unhandled promise rejection:', event.reason);
});
成功
1
2
3

当发生未处理的 Promise 错误时,控制台将会输出类似如下的错误信息:

Uncaught (in promise) xxx
(anonymous) @ VM207:1
成功
1
2

需要注意的是,unhandledrejection 事件不会捕获已经被处理的 Promise 错误。如果一个 Promise 被拒绝,但被处理了,那么不会触发该事件。

除了使用 unhandledrejection 事件捕获异步错误,还可以在 Promise 调用链中添加 catch 处理函数来处理错误,以避免错误未被处理而导致程序出错。

# Vue错误监听

Vue 的错误处理机制与 JavaScript 原生的错误处理机制略有不同,Vue 有自己的错误处理机制,例如在组件的生命周期钩子函数中抛出的错误,不会通过 window.onerror 或者 window.addEventListener('error') 来捕获,而是通过 Vue.config.errorHandler 来处理。这是因为在 Vue 应用中,组件的生命周期钩子函数是 Vue 自己封装的,不会通过原生的错误处理机制来捕获错误,因此需要通过 Vue 提供的配置项来进行错误处理。

Vue.config.errorHandler = function (err, vm, info) {
  console.error('Vue error:', err, '\nVue info:', info);
  // 上报错误信息
}
成功
1
2
3
4

# React错误监听

React 有单独的错误监听方式的原因是因为 React 使用了自己的事件系统,与 DOM 的事件系统有所不同。React 组件的生命周期和渲染方式也不同于传统的 HTML DOM,因此需要一个专门的错误处理机制来捕获和处理错误。

具体来说,React 中的错误分为两种类型:错误边界(Error Boundaries)和未捕获的错误(Unhandled Errors)。错误边界是一种特殊的 React 组件,用于处理其子组件中的错误,而未捕获的错误则是指那些没有被错误边界组件捕获的错误。

为了捕获和处理这两种类型的错误,React 提供了单独的错误处理机制。其中,错误边界可以通过 static getDerivedStateFromError() 和 componentDidCatch() 这两个生命周期函数来捕获和处理子组件中的错误。而未捕获的错误可以通过 window.onerror 和 window.addEventListener('unhandledrejection', handler) 来捕获和处理。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // 错误处理
    console.log('错误对象', error);
    console.log('错误堆栈信息', info);
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24