React捕获异常, 含Hooks方案

共 11051字,需浏览 23分钟

 ·

2022-03-18 10:37

本文适合对React捕获异常,以及对hook封装感兴趣的小伙伴阅读。

欢迎关注前端早茶,与广东靓仔携手共同进阶~

一、前言

在React项目中,因为事件处理程序总是需要写 try/catch,不胜其烦。
虽然可以丢给window.onerror或者 window.addEventListener("error")去处理,但是对错误细节的捕获以及错误的补偿是极其不友好的。

二、一览风姿

我们捕获的范围:
  1. Class的静态同步方法
  2. Class的静态异步方法
  3. Class的同步方法
  4. Class的异步方法
  5. Class的同步属性赋值方法
  6. Class的异步属性赋值方法
  7. Class的getter方法
  8. Hooks方法
getter这里是不是很类似 vue的 计算值,所以以后别说我大React没有计算属性,哈哈。
来来来,一览其风采:

Class组件


interface State {
    price: number;
    count: number;
}

export default class ClassT extends BaseComponent<{}, State> {
    constructor(props) {
        super(props);
        this.state = {
            price: 100,
            count: 1
        }
        this.onIncrease = this.onIncrease.bind(this);
        this.onDecrease = this.onDecrease.bind(this);
    }

    componentDidMount() {
        ClassT.printSomething();
        ClassT.asyncPrintSomething();

        this.doSomethings();
        this.asyncDoSomethings();
    }

    @catchMethod({ message: "printSomething error", toast: true })
    static printSomething() {
        throw new CatchError("printSomething error: 主动抛出");
        console.log("printSomething:"Date.now());
    }

    @catchMethod({ message: "asyncPrintSomething error", toast: true })
    static async asyncPrintSomething() {
        const { run } = delay(1000);
        await run();
        throw new CatchError("asyncPrintSomething error: 主动抛出");
        console.log("asyncPrintSomething:"Date.now());
    }

    @catchGetter({ message: "计算价格失败", toast: true })
    get totalPrice() {
        const { price, count } = this.state;
        // throw new Error("A");
        return price * count;
    }

    @catchMethod("增加数量失败")
    async onIncrease() {

        const { run } = delay(1000);
        await run();

        this.setState({
            count: this.state.count + 1
        })
    }

    @catchMethod("减少数量失败")
    onDecrease() {
        this.setState({
            count: this.state.count - 1
        })
    }

    @catchInitializer({ message: "catchInitializer error", toast: true })
    doSomethings = () => {
        console.log("do some things");
        throw new CatchError("catchInitializer error: 主动抛出");
    }

    @catchInitializer({ message: "catchInitializer async error", toast: true })
    asyncDoSomethings = async () => {
        const { run } = delay(1000);
        await run();
        throw new CatchError("catchInitializer async error: 主动抛出");
    }

    render() {
        const { onIncrease, onDecrease } = this;
        const totalPrice = this.totalPrice;

        return             padding: "150px",
            lineHeight: "30px",
            fontSize: "20px"
        }}>
            
价格:{this.state.price}</div>
            
数量:1div>
            

                增加数量</button>
                减少数量button>
            </div>
            
{totalPrice}div>
        </div>
    }

}
再看看函数式组件,就是大家关注的Hooks,包装出useCatch,底层是基于useMemo
const HooksTestView: React.FC = function (props{

    const [count, setCount] = useState(0);

    
    const doSomething  = useCatch(async function(){
        console.log("doSomething: begin");
        throw new CatchError("doSomething error")
        console.log("doSomething: end");
    }, [], {
        toast: true
    })

    const onClick = useCatch(async (ev) => {
        console.log(ev.target);
        setCount(count + 1);

        doSomething();


        const d = delay(3000() => {
            setCount(count => count + 1);
            console.log()
        });
        console.log("delay begin:"Date.now())
        await d.run();
        console.log("delay end:"Date.now())
        console.log("TestView"this);
        (d as any).xxx.xxx.x.x.x.x.x.x.x.x.x.x.x
        // throw new CatchError("自定义的异常,你知道不")
    },
        [count],
        {
            message: "I am so sorry",
            toast: true
        });

    return 

        
点我</button>div>
        
{count}</div>
    div>
}

export default React.memo(HooksTestView);

三、优化

封装getOptions方法

// options类型白名单
const W_TYPES = ["string""object"];

export function getOptions(options: string | CatchOptions{
    const type = typeof options;
    let opt: CatchOptions;
    
    if (options == null || !W_TYPES.includes(type)) { // null 或者 不是字符串或者对象
        opt = DEFAULT_ERRPR_CATCH_OPTIONS;
    } else if (typeof options === "string") {  // 字符串
        opt = {
            ...DEFAULT_ERRPR_CATCH_OPTIONS,
            message: options || DEFAULT_ERRPR_CATCH_OPTIONS.message,
        }
    } else { // 有效的对象
        opt = { ...DEFAULT_ERRPR_CATCH_OPTIONS, ...options }
    }

    return opt;
}

定义默认处理函数

/**
 * 
 * @param err 默认的错误处理函数
 * @param options 
 */

function defaultErrorHanlder(err: any, options: CatchOptions{
    const message = err.message || options.message;
    console.error("defaultErrorHanlder:", message, err);
}

区分同步方法和异步方法

export function observerHandler(fn: AnyFunction, context: any, callback: ErrorHandler{
    return async function (...args: any[]{
        try {
            const r = await fn.call(context || this, ...args);
            return r;
        } catch (err) {
            callback(err);
        }
    };
}

export function observerSyncHandler(fn: AnyFunction, context: any, callback: ErrorHandler{
    return function (...args: any[]{
        try {
            const r = fn.call(context || this, ...args);
            return r;
        } catch (err) {
            callback(err);
        }
    };
}

具备多级选项定义能力

export default function createErrorCatch(handler: ErrorHandlerWithOptions, 
baseOptions: CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS
{

    return {
        catchMethod(options: CatchOptions | string = DEFAULT_ERRPR_CATCH_OPTIONS) {
            return catchMethod({ ...baseOptions, ...getOptions(options) }, handler)
        }   
    }
}

自定义错误处理函数

export function commonErrorHandler(error: any, options: CatchOptions{    
    try{
        let message: string;
        if (error.__type__ == "__CATCH_ERROR__") {
            error = error as CatchError;
            const mOpt = { ...options, ...(error.options || {}) };

            message = error.message || mOpt.message ;
            if (mOpt.log) {
                console.error("asyncMethodCatch:", message , error);
            }

            if (mOpt.report) {
                // TODO::
            }

            if (mOpt.toast) {
                Toast.error(message);
            }

        } else {

            message = options.message ||  error.message;
            console.error("asyncMethodCatch:", message, error);

            if (options.toast) {
                Toast.error(message);
            }
        }
    }catch(err){
        console.error("commonErrorHandler error:", err);
    }
}


const errorCatchInstance = createErrorCatch(commonErrorHandler);

export const catchMethod = errorCatchInstance.catchMethod; 

四、增强

支持getter

看一下catchGetter的使用

class Test {

    constructor(props) {
        super(props);
        this.state = {
            price: 100,
            count: 1
        }

        this.onClick = this.onClick.bind(this);
    }

    @catchGetter({ message: "计算价格失败", toast: true })
    get totalPrice() {
        const { price, count } = this.state;
        throw new Error("A");
        return price * count;
    }
    
      render() {   
        const totalPrice = this.totalPrice;

        return 

            
价格:{this.state.price}</div>
            
数量:1div>
            
{totalPrice}</div>
        div>
    }
    
}

实现

/**
 * class {  get method(){} }
 * @param options 
 * @param hanlder 
 * @returns 
 */

export function catchGetter(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, 
hanlder: ErrorHandlerWithOptions = defaultErrorHanlder
{

    let opt: CatchOptions = getOptions(options);

    return function (_target: any, _name: string, descriptor: PropertyDescriptor{
        const { constructor } = _target;
        const { get: oldFn } = descriptor;

        defineProperty(descriptor, "get", {
            value: function () {
                // 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 oldFn();
                }
                // 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 oldFn();
                }
                const boundFn = observerSyncHandler(oldFn, thisfunction (error: Error) {
                    hanlder(error, opt)
                }
);
                (boundFn as any)._bound = true;
            
                return boundFn();
            }
        });

        return descriptor;
    }

}

支持属性定义和赋值

class Test{
    @catchInitializer("nono")
    doSomethings = ()=> {
        console.log("do some things");
    }
}

实现

export function catchInitializer(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, hanlder: ErrorHandlerWithOptions = defaultErrorHanlder){

    const opt: CatchOptions = getOptions(options);

     return function (_target: any, _name: string, descriptor: any{

        console.log("debug....");
        const initValue = descriptor.initializer();
        if (typeof initValue !== "function") {
            return descriptor;
        }

        descriptor.initializer = function() {
            initValue.bound = true;
            return observerSyncHandler(initValue, thisfunction (error: Error{
                hanlder(error, opt)
            });
        };
        return descriptor;
    }
}

支持Hooks

使用Demo:


const TestView: React.FC = function (props{

    const [count, setCount] = useState(0);

    
    const doSomething  = useCatch(async function(){
        console.log("doSomething: begin");
        throw new CatchError("doSomething error")
        console.log("doSomething: end");
    }, [], {
        toast: true
    })

    const onClick = useCatch(async (ev) => {
        console.log(ev.target);
        setCount(count + 1);

        doSomething();

        const d = delay(3000() => {
            setCount(count => count + 1);
            console.log()
        });
        console.log("delay begin:"Date.now())

        await d.run();
        
        console.log("delay end:"Date.now())
        console.log("TestView"this)
        throw new CatchError("自定义的异常,你知道不")
    },
        [count],
        {
            message: "I am so sorry",
            toast: true
        });

    return 

        
点我</button>div>
        
{count}</div>
    div>
}

export default React.memo(TestView);

实现: 其基本原理就是利用 useMemo和之前封装的observerHandler,寥寥几行代码就实现了。


export function useCatch<T extends (...args: any[]) => any>(callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS): T {    

    const opt =  useMemo( ()=> getOptions(options), [options]);
    
    const fn = useMemo((..._args: any[]) => {
        const proxy = observerHandler(callback, undefinedfunction (error: Error{
            commonErrorHandler(error, opt)
        });
        return proxy;

    }, [callback, deps, opt]) as T;

    return fn;
}

文章转载于:云的世界

声明:文章著作权归作者所有,如有侵权,请联系小编删除。

五、最后

具备类似功能的库:
catch-decorator
catch-decorator-ts
catch-error-decorator

关注我,一起携手进阶

欢迎关注前端早茶,与广东靓仔携手共同进阶~

浏览 132
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报