????);

????setTimeout(()?=>?{

??????console.log(a);

????});

????new?Promise((resolve,?reject)?=>?{

??????console.log(a);

????});

????console.log(b)

????var?f=e,?//语法异常

??</script>

复制代码


在此过程中,资源文件都是不存在的,我们发现window.addEventListener('error')依旧不能捕获语法错误,Promise异常和iframe异常。

对于语法错误我们可以在编译过程中捕获,,Promise异常已在上文中给出解决方案,现在还剩下iframe异常需要单独处理了。

#### (5) iframe异常

1.用法

window.frames[0].onerror?=?function?(message,?source,?lineno,?colno,?error)?{

????console.log(‘捕获到 iframe 异常:’,{message,?source,?lineno,?colno,?error});

????return?true;

};

复制代码


2.动机

用来专门捕获iframe加载过程中的异常。

3.范围

很遗憾,结果并不令人满意,在实际的测试过程中,该方法未能捕获到异常。

#### (6) React中捕获异常

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

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

*   事件处理

*   异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)

*   服务端渲染

*   它自身抛出来的错误(并非它的子组件)

如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class?ErrorBoundary?extends?React.Component?{

??constructor(props)?{

????super(props);

????this.state?=?{?hasError:?false?};

??}

??static?getDerivedStateFromError(error)?{

????//?更新?state?使下一次渲染能够显示降级后的?UI

????return?{?hasError:?true?};

??}

??componentDidCatch(error,?errorInfo)?{

????//?你同样可以将错误日志上报给服务器

????logErrorToMyService(error,?errorInfo);

??}

??render()?{

????if?(this.state.hasError)?{

??????//?你可以自定义降级后的?UI?并渲染

??????return?Something?went?wrong.;

????}

????return?this.props.children;?

??}

}

复制代码


错误边界的工作方式类似于 JavaScript 的 catch {},不同的地方在于错误边界只针对 React 组件。只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

**以上引用自React 官网。**

#### (7) Vue中捕获异常

Vue.config.errorHandler?=?function?(err,?vm,?info)?{

??//?handle?error

??//?info?是?Vue?特定的错误信息,比如错误所在的生命周期钩子

??//?只在?2.2.0+?可用

}

复制代码


指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。

*   从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃。

*   从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。

*   从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。

**以上引用自Vue 官网。**

#### (8) http请求异常

1.用法

以axios为例,添加响应拦截器

axios.interceptors.response.use(function?(response)?{

????//?对响应数据做点什么

????//?response?是请求回来的数据

????return?response;

??},?function?(error)?{

????//?对响应错误做点什么

????return?Promise.reject(error)

??}

)

复制代码


2.动机

用来专门捕获HTTP请求异常

五、项目实践

------

在提出了这么多的解决方案之后,相信大家对具体怎么用还是存在一些疑惑。那么接下来,我们真正的进入实践阶段吧!

我们再次回顾一下我们需要解决的问题是什么?

*   语法错误

*   事件异常

*   HTTP请求异常

*   静态资源加载异常

*   Promise 异常

*   Iframe 异常

*   页面崩溃

捕获异常是我们的最终目标吗?并不是,回到解决问题的背景下,相比于页面崩溃或点不动,在适当的时机,以一种适当的方式去提醒用户当前发生了什么,无疑是一种更友好的处理方式。

结合到项目中,具体实践起来有如下两种方案:

*   1.代码中通过大量的try catch/Promise.catch来捕获,捕获不到的使用其他方式进行兜底

*   2.通过框架提供的机制来做,再对不能捕获的进行兜底

方案一无疑不是很聪明的样子...这意味着要去改大量的原有代码,心智负担成倍数增加。方案二则更加明智,通过在底层对错误进行统一处理,无需变更原有逻辑。

到项目中,使用的是React框架,React正好提供了一种捕获异常的机制(上文已提及)并做降级处理,但是细心的小伙伴发现了,react并不能捕获如下四种错误:

*   事件处理

*   异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)

*   服务端渲染

*   它自身抛出来的错误(并非它的子组件)

对于第三点服务端渲染错误,项目中并没有适用的场景,此次不做重点分析。我们重点分析第一点和第二点。

我在这里先抛出几个问题,大家先做短暂的思考:

*   1.若事件处理和异步代码的错误导致页面crash,我们该如何预防?

*   2.如何对ErrorBounary进行兜底?相比一个按钮点击无效,如何更加友好的提示用户?

先来看第一个问题,若事件处理和异步代码的错误导致页面崩溃:

const?Test?=?()?=>?{

??const?[data,?setData]?=?useState([]);

??return?(

????<div

??????onClick={()?=>?{

????????setData(”);

??????}}

????>

??????{data.map((s)?=>?s.i)}

????</div>

??);

};

复制代码


此段代码在正常渲染期间是没问题的,但在触发了点击事件之后会导致页面异常白屏,如果在外面套上我们的ErrorBounday组件,情况会是怎么样呢?

答案是依然能够捕获到错误,并能够对该组件进行降级处理!

此时有些小伙伴已经察觉到了,错误边界只要是在渲染期间都是可以捕获错误的,无论首次渲染还是二次渲染。流程图如下:

![](https://s2.51cto.com/images/20210927/1632674538770246.jpg)

image.png

第一个问题原来根本就不是问题,这本身就是一个闭环,不用我们解决!

再来看看第二个问题:

对于事件处理和异步代码中不会导致页面崩溃的代码:

const?Test?=?()?=>?{

??return?(

????<button

??????onClick={()?=>?{

????????[].map((s)?=>?s.a.b);

??????}}

????>

??????点击

????</button>

??);

};

复制代码


button按钮可正常点击,但是该点击事件的内部逻辑是有问题的,导致用户点击该按钮本质是无效的。此时若不及时给与友好提示,用户只会陷入抓狂中....

那么有没有办法对ErrorBoundary进行兜底呢?即可以捕获异步代码或事件处理中的错误。

上文提到的window.addEventListener('error')正好可以解决这个问题。理想状态下:

![](https://s2.51cto.com/images/20210927/1632674539817121.jpg)而真正的执行顺序确实这样的:

![](https://s2.51cto.com/images/20210927/1632674539685504.jpg)

1625105438(1).png

在真正执行的过程中,window.addEventListener('error')是先于ErrorBoundary捕获到错误的,这就导致当error事件捕获到错误时,他并不知道该错误是否会导致页面崩溃,不知道该给予怎样的提示,到底是对页面进行降级处理还是只做简单的报错提示?

问题似乎就卡在这了....

那能否通过一种有效的途径告诉error事件:ErrorBoundary已经捕获到了错误,你不需要处理!亦或者是ErrorBoundary未能捕获到错误,这是一个异步错误/事件错误,但不会引起页面崩溃,你只需要提示用户!

答案肯定是有的,比如建立一个nodeJs服务器,通过webSocket去通知,但是这样做不仅麻烦,还会有一定的延迟。

在笔者苦思冥想之际,在某个静悄悄的夜晚,突然灵感一现。为什么我们非要按照他规定的顺序执行呢?我们能不能尝试改变他的执行顺序,让错误捕获回到我们理想中的流程来呢?

改变思路之后,我们再思考有什么能改变代码执行顺序吗?没错,异步事件!

window.addEventListener(‘error’,?function?(error)?{

????????setTimeout(()=>{

??????????console.log(error,?’error错误’);

????????})

??????});

复制代码


当给error事件的回调函数加入setTimeout后,捕获异常的流程为:

![](https://s2.51cto.com/images/20210927/1632674539447125.jpg)

image.png

现在就可以通知error事件到底页面崩溃了没有,到底需不需要它的处理!上代码:

class?ErrorBoundary?extends?React.Component?{

??constructor(props)?{

????super(props);

????this.state?=?{?hasError:?false?};

??}

??static?getDerivedStateFromError(error)?{

????//?更新?state?使下一次渲染能够显示降级后的?UI

????return?{?hasError:?true?};

??}

??componentDidCatch(error,?errorInfo)?{

????//?你同样可以将错误日志上报给服务

????logErrorToMyService(error,?errorInfo);

????

????//告诉error事件?ErrorBoundary已处理异常

?????localStorage.setItem("ErrorBoundary",true)

??}

??render()?{

????if?(this.state.hasError)?{

??????//?你可以自定义降级后的?UI?并渲染

??????return?Something?went?wrong.;

????}

????return?this.props.children;?

??}

}

复制代码


 window.addEventListener('error',?function?(error)?{

????????setTimeout(()?=>?{

??????????//进来代表一定有错误?判断ErrorBoundary中是否已处理异常

??????????const?flag?=?localStorage.getItem('ErrorBounary');

??????????if?(flag)?{

????????????//进入了ErrorBounary?错误已被处理?error事件不用处理该异常

????????????localStorage.setItem('ErrorBounary',?false);?//重置状态

??????????}?else?{

????????????//未进入ErrorBounary?代表此错误为异步错误/事件错误

????????????logErrorToMyService(error,?errorInfo);??//?你可以将错误日志上报给服务

????????????//判断具体错误类型

????????????if?(error.message.indexOf('TypeError'))?{

??????????????alert('这是一个TypeError错误,请通知开发人员');

????????????}?else?if?(error.message.indexOf('SyntaxError'))?{

??????????????alert('这是一个SyntaxError错误,请通知开发人员');

????????????}?else?{

??????????????//在此次给与友好提示

????????????}

??????????}

????????});

??????});

复制代码 

最后,通过我们的努力,当页面崩溃时,及时进行降级处理;当页面未崩溃,但有错误时,我们及时的告知用户,并对错误进行上报,达到预期的效果。

六、扩展


1.设置采集率

若是错误实在太多,比如有时候代码进入死循环,错误量过多导致服务器压力大时,可酌情降低采集率。比如采集30%:


 if?(Math.random()?<?0.3)?{

????????//上报错误

????????logErrorToMyService(error,?errorInfo);

??????}

复制代码 

2.提效

解决上面这些问题后,大家难免会有疑问:那每一个组件都要去套一层ErrorBoundary组件,这工作量是不是有点大….而且有一些老代码,嵌套的比较深,改起来心理负担也会比较大。那有没有办法将其作为一个配置项,配置完之后,编译时自动套上一层ErrorBoundary组件呢?这个我们下次在做探讨!

3.可配置

能否将ErrorBoundary扩展成可传入自定义UI的组件呢?这样大家通过定制化UI,在不同的场景进行不同的降级处理。

同样,这一块我们下次再讨论!

七、总结


异常处理是高质量软件开发中的一个基本部分,但是在许多情况下,它们会被忽略,或者是不正确的使用,而处理异常只是保证代码流程不出错,重定向到正确的程序流中去。

本文从前端错误类型出发,从try catch逐步揭开错误异常神秘的面纱,再通过一系列的操作对异常进行监控和捕获,最后达到提升用户体验,上报监控系统的效果。

八、思考


  • Promise.catch 和 try catch 捕获异常有什么区别?

打开全栈工匠技能包-1小时轻松掌握SSR

剖析前端异常及降级处理,大牛推荐

两小时精通jq+bs插件开发

剖析前端异常及降级处理,大牛推荐

生产环境下如歌部署Node.js

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

剖析前端异常及降级处理,大牛推荐

网易内部VUE自定义插件库NPM集成

剖析前端异常及降级处理,大牛推荐

谁说前端不用懂安全,XSS跨站脚本的危害

剖析前端异常及降级处理,大牛推荐

webpack的loader到底是什么样的?两小时带你写一个自己loader

剖析前端异常及降级处理,大牛推荐