什么是异常

是指用户在使用应用时,无法得到预期的结果。不同的异常带来的后果程度不同,轻则引起用户使用不悦,重则导致产品无法使用,从而使用户丧失对产品的认可。

为什么要处理异常

  • 增强用户体验
  • 远程定位问题
  • 无法复现问题,特别是移动端,各种原因,可能是系统版本,机型等等

前端有哪些异常

异常频率
JavaScript异常(语法错误、代码错误)经常
静态资源加载异常(img、js、css)偶尔
Ajax 请求异常偶尔
promise异常较少
iframe 异常较少

如何捕获异常

try-catch

try-catch 只能捕获同步运行错误,对语法和异步错误却捕获不到。

1、同步运行错误

try {    kill;} catch(err) {    console.error('try: ', err);}

结果:try: ReferenceError: kill is not defined

2、无法捕获语法错误

try {    let name = '1;} catch(err) {    console.error('try: ', err);}

结果:Unterminated string constant

编译器能够阻止运行语法错误。

3、无法捕获异步错误

try {    setTimeout(() => {        undefined.map(v => v);    }, 1000);} catch(err) {    console.error('try: ', err);}

结果:Uncaught TypeError: Cannot read property 'map' of undefined

window.onerror

当JavaScript运行时错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。若该函数返回true,则阻止执行默认事件处理函数。

1、同步运行错误

/*** @param {String}  message   错误信息* @param {String}  source    出错文件* @param {Number}  lineno    行号* @param {Number}  colno     列号* @param {Object}  error     error对象*/window.onerror = (message, source, lineno, colno, error) => {    console.error('捕获异常:', message, source, lineno, colno, error);    return true;};kill;

结果:捕获异常: Uncaught ReferenceError: kill is not defined

2、无法捕获语法错误

/*** @param {String}  message   错误信息* @param {String}  source    出错文件* @param {Number}  lineno    行号* @param {Number}  colno     列号* @param {Object}  error     error对象*/window.onerror = (message, source, lineno, colno, error) => {    console.error('捕获异常:', message, source, lineno, colno, error);    return true;};let name = '1;

结果:Unterminated string constant

编译器能够阻止运行语法错误。

3、异步错误

/*** @param {String}  message   错误信息* @param {String}  source    出错文件* @param {Number}  lineno    行号* @param {Number}  colno     列号* @param {Object}  error     error对象*/window.onerror = (message, source, lineno, colno, error) => {    console.error('捕获异常:', message, source, lineno, colno, error);    return true;};setTimeout(() => {    undefined.map(v => v);}, 1000);

结果:捕获异常: Uncaught TypeError: Cannot read property 'map' of undefined

window.addEventListener('error')

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

<script>window.addEventListener('error', (err) => {    console.error('捕获异常:', err);}, true);</script><img src="./test.jpg" />

结果:捕获异常:Event {isTrusted: true, type: "error", target: img, currentTarget: Window, eventPhase: 1, …}

window.addEventListener('unhandledrejection')

当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

window.addEventListener("unhandledrejection", (err) => {    err.preventDefault();    console.error('捕获异常:', err);});Promise.reject('promise');

结果:捕获异常:PromiseRejectionEvent {isTrusted: true, promise: Promise, reason: "promise", type: "unhandledrejection", target: Window, …}

Vue

Vue.config.errorHandler = (err, vm, info) => {  console.error('捕获异常:', err, vm, info);}

React

React 16,提供了一个内置函数componentDidCatch,使用它可以非常简单的获取到React下的错误信息。

componentDidCatch(error, info) {    console.error('捕获异常:', error, info);}

但是,推荐ErrorBoundary

用户界面中的JavaScript错误不应破坏整个应用程序。为了为React用户解决此问题,React 16引入了“错误边界”的新概念。

新建ErrorBoundary.jsx组件:

import React from 'react';import { Result, Button } from 'antd';class ErrorBoundary extends React.Component {    constructor(props) {        super(props);        this.state = { hasError: false, info: '' };    }      static getDerivedStateFromError(error) {        return { hasError: true };    }    componentDidCatch(error, info) {        this.setState({            info: error + ''        });    }      render() {        if (this.state.hasError) {            // 你可以渲染任何自定义的降级 UI            return (                <Result                    status="500"                    title="500"                    subTitle={this.state.info}                    extra={<Button type="primary">Report feedback</Button>}                />            );        }          return this.props.children;     }}export default ErrorBoundary;

使用:

<ErrorBoundary>    <App /></ErrorBoundary>

注意

错误边界不会捕获以下方面的错误:

  • 事件处理程序
  • 异步代码(例如setTimeout或requestAnimationFrame回调)
  • 服务器端渲染
  • 在错误边界本身(而不是其子级)中引发的错误

iframe

由于浏览器设置的“同源策略”,无法非常优雅的处理iframe异常,除了基本属性(例如其宽度和高度)之外,无法从iFrame获得很多信息。

<script>    document.getElementById("myiframe").onload = () => {        const self = document.getElementById('myiframe');                try {            (self.contentWindow || self.contentDocument).location.href;        } catch(err) {            console.log('捕获异常:' + err);        }    };</script><iframe id="myiframe" src="https://nibuzhidao.com" frameBorder="0" />

Sentry

业界非常优秀的一款监控异常的产品,作者也是用的这款,文档齐全。

需要上报哪些信息

  • 错误id
  • 用户id
  • 用户名
  • 用户IP
  • 设备
  • 错误信息
  • 游览器
  • 系统版本
  • 应用版本
  • 机型
  • 时间戳
  • 异常级别(error、warning、info)

异常上报

1、Ajax发送数据
2、动态创建img标签

如果异常数据量大,导致服务器负载高,调整发送频率(可以考虑把异常信息存储在客户端,设定时间阀值,进行上报)或设置采集率(采集率应该通过实际情况来设定,随机数,或者某些用户特征都是不错的选择)。

流程图

博客

欢迎关注我的博客

参考文献

  • 如何优雅处理前端异常?
  • React
  • MDN
  • Vue