阅读 1148

前端异常处理

记一次寻找bug的过程....

忙了一整天,bug都解决的差不多了。看看时间,已经是下午五点半了,寻思还有半个小时做点什么?

突然想起前两天接到的任务:要求优化前端报错异常捕获。

刚接到这个任务,大脑第一反应:so easy! window.onerror不就可以了?

当我把这个方案提出时,领导接着说,这样确实能捕获,但我们更多是需要对出错后的降级处理~(及时确认需求的重要性)

降级处理?

想想也不难,不就是显示出一个报错页面嘛,当window.onerror检测到错误时,直接重定向到一个错误UI页面不就ok!今天六点准时下班应该是稳了!想到这里,不禁开始飘飘然!

然而,领导再次提出,这样做对用户不太友好,我们只需要将报错的组件页面进行降级处理即可,不能影响其他tab栏或者页面的访问。

这......

经过google的一顿搜索,发现这种问题不少。给出的方案有如下几种:

  • try catch
  • Promise.catch
  • window.onerror
  • window.addEventListener('error',cb)
  • ...

发现方案确实一大堆,但真正符合项目需求的基本上没有~~

首先项目中出错的地方很多,项目中写大量的try catch/Promise.catch显然不太友好,直接放弃。至于window.onerror/window.addEventListener('error',cb) 这两个可以适当考虑一下。

通过进一步的调研,发现window.onerror不是万能的,当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.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});
}
复制代码

通过调研发现,onerror 无法捕获语法错误;以及是静态资源异常,或者接口异常,错误都无法捕获到。这也太坑爹了....

要知道在实际项目开发中,有大量的对对象链式操作,比如:

跟后端协商的数据类型:data:{a:{b:c:{d}}}

当我们要取数据 d 时是通过data.a.b.c.d这样来获取的,只要其中一环出问题(后端接口返回数据不对或者接口被攻击失效等),在线上就会白屏,这种情况非常常见。

单凭无法捕获语法错误,直接pass掉。

接下来再去看看window.addEventListener('error',cb),发现通过对error事件的监听,可以除了监听不到http请求的错误除外,其他资源加载失败以及语法错误都能监听到。

咦,既然这样,那这个问题不是就解决了?我们通过http拦截器监听http请求的错误,再通过window.addEventListener('error',cb) 监听其他错误不就好了!想想就觉得兴奋,感觉差不多快下班了!

然而,当我开始动手时,想起了最开始的需求,要求对错误组件UI进行降级处理...,这怎么实现....又陷入了沉思。

降级处理?嗯?React官方不是有降级处理的说明!

灵感一现,赶紧去查看文档,果然,React nb,但是却发现

错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多)
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

这.....

事件处理和异步代码我们可以通过window.addEventListener('error',cb) 来捕获 ,至于服务端渲染,目前没有用到,不考虑,它自身抛出的错误?直接复制你官方的组件,我就不信还能出错?叉会腰!

好了,现在方案已定,开始撸代码!

import React from 'react';

export interface IErrorBoundaryProps {
  children: object;
}
export interface IErrorBoundaryState {
  hasError: boolean;
}
export default class ErrorBoundary extends React.Component<
  IErrorBoundaryProps,
  IErrorBoundaryState
> {
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI

    console.log(error);
    return { hasError: true };
  }
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  // componentDidCatch(error, errorInfo) {
  //   // 你同样可以将错误日志打印出来
  //   console.log(error);
  //   console.log(errorInfo);
  // }
  render() {
    console.log(this.state.hasError, 'this.state.hasError');
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

复制代码

将ErrorBoundary套在路由组件中,不就可以对路由页面进行降级处理啦!

<Route
  key={item.path}
  path={item.path}
  render={(props) => (
    <ErrorBoundary>
      <item.component {...props} routes={item.routes} />
    </ErrorBoundary>
  )}
/>;
复制代码

好了!测试一下准备下班!

随便在组件中写个错误代码,看看会不会触发。然而:

4f7322077eba81fc72ff7dc78975e2c.png

咦,这怎么回事?确实达到了预期的效果,不影响其他功能的使用,但是这UI,怎么和我想象的不一样?

苦思冥想,问题出在哪里?

莫非是webpack配置中 overlay: { warnings: false, errors: true }影响了?

不对,如果是这两个属性触发的话,会导致全屏显示,根本不会是这样只在路由中显示。那问题出在哪里呢?

而且也没有触发window.addEventListener('error',cb),说明并没有冒泡到window中,那到底是在哪一环被截取了呢?

通过仔细检查,我发现触发异常的这个组件,在上面的某一层中套了ErrorBoundary,最开始并没有引起我的警觉,后来仔细查看,发现

微信图片_20210508204448.png

莫非是这玩意引起的?

去掉该组件,自定义的错误UI显示出来了!卧槽,大意了啊!

然而此时天已黑,完~~~

总结:通过ErrorBoundary捕获渲染过程中的异常,window.addEventListener('error',cb) 捕获资源或者其他错误的异常,http拦截器捕获请求异常,让整个项目的异常可控。

后续有更好的方案和思路再更新~

后续:juejin.cn/post/697956…

文章分类
前端
文章标签