86

React中如何优雅的捕捉事件错误

 5 years ago
source link: https://xiangwenhu.github.io/blog/2018/07/25/react/decorate-catchError/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理。

我就想问问大家react的错误怎么捕捉呢? 这个时候:

  • 小白:怎么处理?
  • 小白+: ErrorBoundary
  • 小白++: ErrorBoundary, try catch
  • 小白#: ErrorBoundary, try catch, window.onerror
  • 小白##: 这个是个严肃的问题,我知道*种处理方式,你有什么好的方案?

正题

小白#回答的基本就是解决思路。我们来一个一个简单说说。

1. EerrorBoundary

EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError。

关于EerrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。

Error boundaries在rendering,lifeCyclemethod或处于他们树层级之下的构造函数中捕获错误

哦,原来如此。 怎么用

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>

重点:error boundaries并不会捕捉这些错误:

  • 事件处理器
  • 异步代码
  • 服务端的渲染代码
  • 在error boundaries区域内的错误

2. try catch

简单有效的捕捉

handleClick = () => {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
}

3. window.onerror

超级奥特曼,只是错误信息比较不好分析。

4.其他

  1. http请求
    封装后,专门处理
  2. 状态管理redux,mobx等
    封装拦截 ,我们在项目的应用mobx-state-tree基本都是在model里面拦截的
  3. 其他
    自己看着办啊,都找我,我很忙的。

问题

啊?这么多事件处理和方法都要加try catch啊。 你笨啊window.onerror啊。

onerror是非常好,但是有个问题,错误细节不好分析,有大神说,正则解析。

我不扶墙扶你。

解决

decorator特性,装饰器。 create-react-app创建的app默认是不知此的装饰器的。

不要和我争,github地址上人家写着呢 can-i-use-decorators?

那问题又来了,如何支持装饰器。

  • 场景一:自己构建的项目
    那还不简单的飞起
  • 场景二: create-react-app脚手架创建的项目
    1. npm run eject
    2. react-app-rewired
      const {injectBabelPlugin} = require('react-app-rewired');
      
      
      /* config-overrides.js */
      module.exports = {
          webpack: function override(config, env) {       
              // babel  7
              config = injectBabelPlugin('transform-decorators-legacy',config)
              // babel 6
              config = injectBabelPlugin('transform-decorators',config)
              return config;
          }
      }
      

关于装饰器这里不做过多的说明,修改类的行为。

这里又有几个点

  1. 装饰方法 装饰类 装饰getter, setter都可以,我们选在装饰方法和类
  2. 装饰类,如何排除系统内置方法和继承的方法
  3. 装饰的时候有参和无参数怎么处理

我们先写一个来检查内置方法的方法, 不够自己补全

const PREFIX = ['component', 'unsafe_']
const BUILTIN_METHODS = [
    'constructor',
    'render',
    'replaceState',
    'setState',
    'isMounted',
    'replaceState'
]
// 检查是不是内置方法
function isBuiltinMethods(name) {
    if (typeof name !== 'string' || name.trim() === '') {
        return false
    }
    // 以component或者unsafe_开头
    if (PREFIX.some(prefix => name.startsWith(prefix)))) {
        return true
    }
    // 其他内置方法
    if (BUILTIN_METHODS.includes(name)) {
        return true
    }
    return false
}

再弄一个装饰方法的方法, 这个方法参考了 autobind.js

handleError是自己的错误处理函数,这里没有写出来

// 监听方法

 function createDefaultSetter(key) {
    return function set(newValue) {
        Object.defineProperty(this, key, {
            configurable: true,
            writable: true,
            // IS enumerable when reassigned by the outside word
            enumerable: true,
            value: newValue
        });

        return newValue;
    };
}


function observerHandler(fn, callback) {
    return (...args) => {
        try {
            fn(...args)
        } catch (err) {
            callback(err)
        }
    }
}
//方法的装饰器, params是额外的参数
function catchMethod(target, key, descriptor, ...params) {

    if (typeof descriptor.value !== 'function') {
        return descriptor
    }
    const { configurable, enumerable, value: fn } = descriptor
    return {
        configurable,
        enumerable,

        get() {
            // Class.prototype.key lookup
            // Someone accesses the property directly on the prototype on which it is
            // actually defined on, i.e. Class.prototype.hasOwnProperty(key)
            if (this === target) {
                return fn;
            }

            // Class.prototype.key lookup
            // Someone accesses the property directly on a prototype but it was found
            // up the chain, not defined directly on it
            // i.e. Class.prototype.hasOwnProperty(key) == false && key in Class.prototype
            if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
                return fn;
            }

            const boundFn = observerHandler(fn.bind(this), err => {
                handleError(err, target, key, ...params)
            })

            defineProperty(this, key, {
                configurable: true,
                writable: true,
                // NOT enumerable when it's a bound method
                enumerable: false,
                value: boundFn
            });

            boundFn.bound = true
            return boundFn;
        },
        set: createDefaultSetter(key)
    };
}

再来一个装饰类的

/**
 * 检查是不是需要代理
 * @param {*} method 
 * @param {*} descriptor 
 */
function shouldProxy(method, descriptor) {
    return typeof descriptor.value === 'function'
        && !isBuiltinMethods(method)
        && descriptor.configurable
        && descriptor.writable
        && !descriptor.value.bound
}

function catchClass(targetArg, ...params) {
    // 获得所有自定义方法,未处理Symbols
    const target = targetArg.prototype || targetArg
    let descriptors = getOwnPropertyDescriptors(target)
    for (let [method, descriptor] of Object.entries(descriptors)) {
        if (shouldProxy(method, descriptor)) {
            defineProperty(target, method, catchMethod(target, method, descriptors[method], ...params))
        }
    }
}

最后暴露一个自动识别方法和类的方法

/**
 * 
 * 未拦截getter和setter
 * 未拦截Symbols属性
 */
export default function catchError(...args) {
    const lastArg = args[args.length - 1]
    // 无参数方法
    if (isDescriptor(lastArg)) {
        return catchMethod(...args)
    } else {
        // 无参数class?? 需要改进
        if (args.length === 1 && typeof args[0] !== 'string') {
            return catchClass(...args)
        }
        // 有参
        return (...argsList) => {
            // 有参数方法
            if (isDescriptor(argsList[argsList.length - 1])) {
                return catchMethod(...[...argsList, ...args])
            }
            // 有参数class
            return catchClass(...[...argsList, ...args])
        }
    }
}

基本成型。

怎么调用

装饰类

@catchError('HeHe')
class HeHe extends React.Component {
    
    state = {
        clicked: false
    }
    
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    render(){
        return (
            <input type="button" value="点击我" onClick={this.onClick}/>
        )
    }

}

装饰方法

class HeHe extends React.Component {    
    state = {
        clicked: false
    }
    
    @catchError('HeHe onClick')
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    render(){
        return (
            <input type="button" value="点击我" onClick={this.onClick}/>
        )
    }

}

当然你还可以既装饰类又装饰方法, 这个时候方法的装饰优先于类的装饰,不会重复装饰

@catchError('HeHe')
class HeHe extends React.Component {
    
    state = {
        clicked: false
    }
    
    @catchError('HeHe onClick')
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    onClick2(){

    }

    render(){
        return (
            <React.Fragment>
                <input type="button" value="点击我" onClick={this.onClick}/>
                <input type="button" value="点击我2" onClick={this.onClick2}/>
            </React.Fragment>
        )
    }

}

如上,细心的人可以发现, 没有 onClick.bind(this), 是的, catchError会自动完成bind,是不是很cool。

如上,现在的所有的事件处理都会被catchError里面定义的handleError处理,怎么处理就看你自己了。

有人就问了,我要是想捕捉后还要有额外处理的了,比如来个提示框之类的。

这个就取决你的需求和怎么处理,你依旧可以在你的事件处理器try catch。

二是,你没看到@catchError里面可以传递参数么,可以提供额外的错误信息,比如场景,是不是致命错误等等信息。

她解决了你未显示处理的事件处理错误,有没有很优雅,有没有。

你们都说没有的话, 我就放弃前端了,可是我还有老婆孩子要养,所以你们一定要有人说有。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK