javascript ·

前端异常捕获机制

前言

BUG是一个软件工程师永远也绕不开的问题,它可能存在于任何你想不到的地方,有时候用户反馈过来了,你还偏偏无法复现不知道具体是哪出了问题。
因此,对于最接近用户的前端来说,为了能远程定位问题、增强用户体验,异常的捕获和上报至关重要。对于前端编程来说,用JS捕获异常,是最适合、最方便的,都是JS。
我们面对的仅仅只是异常,异常的出现不会直接导致JS引擎崩溃,最多只是终止当前代码的执行。
通常来说我们会有一下几种异常的捕获
- 可疑区域增加 try...catch
- 全局监控JS异常: window.onerror
- 全局监控静态资源异常: window.addEventListener
- 全局捕获没有 catch 的 promise 异常:unhandledrejection
- React componentDidCatch
- 监控网页崩溃

下面我们分别来看一下

try...catch

try...catch只能捕获到同步的运行时错误,对于语法和异步错误无能为力,捕获不到。

try {
  let name = 'oecom';
  console.log(cn);
} catch(e) {
  console.log('捕获到异常:',e);
}

输出结果为:

捕获到异常: ReferenceError: cn is not defined
    at <anonymous>:3:15

但是如果是语法错误是无法捕获到的,如下代码,少了一个引号

try {
  let name = 'oecom';
  console.log(cn);
} catch(e) {
  console.log('捕获到异常:',e);
}

执行之后你会在控制台上看到一行红字:Uncaught SyntaxError: Invalid or unexpected token

语法错误SyntaxError,不管是window.error还是try...catch都没法捕获异常。但是不用担心,在你写好代码按下保存那一刻,编译器会帮你检查是否有语法错误,如果有错误有会有个很明显的红红的波浪线,把鼠标移上去就能看到报错信息。因此,面对SyntaxError语法错误,一定要小心小心再小心

window.onerror

当JS运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror() ,onerror方法会接收到异步执行的异常。

/**
* @param {String} message 错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象(对象)
*/

window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}

如果我们在来执行上面的异步方法,我们会发现成功捕获到了。这里我就不贴结果了,大家可以自行尝试一下。
注意:
- window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出(浏览器接收后报红),否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx
- window.onerror 最好写在所有JS脚本的前面,否则有可能捕获不到错误
- window.onerror无法捕获语法错误
- window.onerror也无法捕获静态资源文件的加载错误

addEventListener 注册全局异常监听

当一项资源(如图片和脚本加载失败),加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror处理函数。这些error事件不会向上冒泡到window, 不过(至少在 Chrome 中)能被单一的window.addEventListener 捕获。

window.addEventListener('error', (error) => {
  console.log('捕获到异常:', error);
}, true)

但是如果想捕获ifream中的异常,写法需要改一下

window.frames[0].onerror = function (message, source, lineno, colno, error) {
  console.log('捕获到 iframe 异常:', {message, source, lineno, colno, error});
};

静态资源加载异常捕获
由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。
注意:
不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。 需要注意避免 window.addEventListener 重复监听。

promise 异常:unhandledrejection

上面说的几种异常捕获都是无法捕获promise抛出的异常,如果在 promise 中使用了 catch ,可以非常方便的捕获到异步 error 。
没有写catch的promise中抛出的错误无法被onerror或try...catch捕获到,所以务必在promise中写catch做异常处理。
有没有一个全局捕获promise的异常呢?答案是有的。 Uncaught Promise Error就能做到全局监听,使用方式:

window.addEventListener("unhandledrejection", function(e){
// e.preventDefault(); // 阻止异常向上抛出
console.log('捕获到异常:', e);
});
Promise.reject('promise error');

所以,正如我们上面所说,为了防止有漏掉的 promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听 Uncaught Promise Error。

React 异常捕获

在React,可以使用ErrorBoundary组件包括业务组件的方式进行异常捕获,配合React 16.0+新出的componentDidCatch API,可以实现统一的异常捕获和日志上报。
我们来举一个小例子,在下面这个 componentDIdCatch(error,info) 里的类会变成一个 error

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

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

使用方法和普通组件的方法一样

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

componentDidCatch() 方法像JS的 catch{} 模块一样工作,但是对于组件,只有 class 类型的组件(class component )可以成为一个 error boundaries 。
实际上,大多数情况下我们可以在整个程序中定义一个 error boundary 组件,之后就可以一直使用它了!
需要注意的是:error boundaries并不会捕捉下面这些错误:
- 事件处理器
- 异步代码
- 服务端的渲染代码
- 在 error boundaries 区域外的错误

如果 render() 函数抛出错误,才会触发该函数。

监控网页崩溃

对于监控网页的崩溃我们可以采用server Worker来实现,首先有以下几点原因
- Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker一般情况下不会崩溃
- Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态
- 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息

参与评论