zoukankan      html  css  js  c++  java
  • React中如何优雅的捕捉事件错误

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

    前话

    人无完人,所以代码总会出错,出错并不可怕,关键是怎么处理。
    我就想问问大家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?

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

    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里面可以传递参数么,可以提供额外的错误信息,比如场景,是不是致命错误等等信息。

    她解决了你未显示处理的事件处理错误,有没有很优雅,有没有。
    你们都说没有的话, 我就放弃前端了,可是我还有老婆孩子要养,所以你们一定要有人说有。

    error-boundaries
    React异常处理
    catching-react-errors

    react进阶之异常处理机制-error Boundaries

    decorator
    core-decorators
    autobind.js

  • 相关阅读:
    Vue之axios基础使用
    Vue + Spring Boot 项目实战(二):使用 CLI 搭建 Vue.js 项目
    解决:'webpack-dev-server' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
    CentOS root用户修改只读文件时提示加! 解决办法
    CentOS 7 源码编译安装 Redis
    CentOS安装Jdk并配置环境变量
    Vue + Spring Boot 项目实战(一):项目简介
    druid 数据源 使用属性文件的一个坑
    scala 学习笔记(07) 一等公民的函数
    linux:手动校准系统时间和硬件CMOS时间
  • 原文地址:https://www.cnblogs.com/cloud-/p/9366234.html
Copyright © 2011-2022 走看看