React, расширенная глава для элегантного захвата исключений, включая решения Hooks.

внешний интерфейс
React, расширенная глава для элегантного захвата исключений, включая решения Hooks.

Это 9-й день моего участия в Gengwen Challenge.Подробности мероприятия смотрите:Обновить вызов

КомпаньонРеагируйте, изящно перехватывайте исключения

предисловие

В проектах React, поскольку обработчики событий всегда нужно писатьtry/catch, более хлопотно.

Хотя можно броситьwindow.onerrorилиwindow.addEventListener("error")справиться с этим, но даЗахват сведений об ошибкетак же какнеправильная компенсациякрайне недружелюбен.

Так на основе стандартного декоратора ES разработана схема захвата обработчиков событий, подробнее в предыдущей статье.Реагируйте, изящно перехватывайте исключения.

В комментариях копатели жалуются, какая сейчас эпоха, а они до сих пор пишутClass? ,Hooks666 Ач.

Вы правы, я хочу идти в ногу со временем и поддерживатьHooks, getterи т.п.

Добавлять

Первоначально он использовался только для перехвата и обработки исключений из программ обработки событий, но на самом деле его можно использовать в любом методе класса..

обзор проблемы

Реагируйте, изящно перехватывайте исключения Проблемы с программой:

  1. Абстракции недостаточно
    Получить параметры, функция обработки ошибок может быть полностью отделена и стать общим методом.
  2. Синхронные методы преобразуются в асинхронные методы.
    Так что теоретически следует различать синхронные и асинхронные схемы.
  3. Что делать, если функция обработки ошибок снова работает неправильно?
  4. Функциональные ограничения

Давайте решим их один за другим.

С одного взгляда

Наш диапазон захвата:

  1. Метод статической синхронизации класса
  2. Статический асинхронный метод класса
  3. Синхронный метод класса
  4. Асинхронные методы класса
  5. Синхронизированный метод присвоения свойств класса
  6. Метод асинхронного присвоения свойств класса
  7. Метод получения класса
  8. Метод крючков

Здесь геттер очень похож?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 <div style={{
            padding: "150px",
            lineHeight: "30px",
            fontSize: "20px"
        }}>
            <div>价格:{this.state.price}</div>
            <div>数量:1</div>
            <div>
                <button onClick={onIncrease}>增加数量</button>
                <button onClick={onDecrease}>减少数量</button>
            </div>
            <div>{totalPrice}</div>
        </div>
    }

}

Давайте взглянем на функциональные компоненты, которые представляют собой хуки, о которых все беспокоятся, в упакованном виде.useCatch, нижний слой основан на useMemo

const HooksTestView: React.FC<Props> = 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 <div>
        <div><button onClick={onClick}>点我</button></div>
        <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; 

усиливать

поддержка геттера

Первый взгляд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 <div>
            <div>价格:{this.state.price}</div>
            <div>数量:1</div>
            <div>{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, this, function (error: Error) {
                    hanlder(error, opt)
                });
                (boundFn as any)._bound = true;
            
                return boundFn();
            }
        });

        return descriptor;
    }

}

Поддержка определения и назначения свойства

Для этого требуется поддержка babel.Подробнее см.babel-plugin-proposal-class-properties

демо можно посмотретьclass-error-catch

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, this, function (error: Error) {
                hanlder(error, opt)
            });
        };
        return descriptor;
    }
}

Крюки поддержки

использовать


const TestView: React.FC<Props> = 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 <div>
        <div><button onClick={onClick}>点我</button></div>
        <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, undefined, function (error: Error) {
            commonErrorHandler(error, opt)
        });
        return proxy;

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

    return fn;
}

Что вы можете спросить здесь, вы только что реализовали захват исключения метода, мойuseEffect, useCallbak, useLayoutПодожди, тебе все равно?

На самом деле, здесь есть в основном две идеи

  1. Отдельно определенные методы на основе useCatch
  2. Напишите еще по одному для каждого крючкаuseXXX

На данный момент, я думаю, это достаточно трудно для вас.

Я просто предлагаю здесь идею, идею, которая не кажется сложной и осуществимой.

Об исходном коде

Поскольку текущий код выполняется непосредственно в нашем фактическом проекте, нет времени на независимое выделение кода в отдельный проект. Студенты, которым нужен весь исходный код, могут связаться со мной.

После этого весь исходный код и примеры будут изолированы.

следовать за

Я уверен, что кто-то спросит, вы используетеObject.defineProperty, видишь лиvueиспользовать обаProxyсбываться.

Да, Proxy мощный, но с ним нужно разбираться в конкретной ситуации.Здесь я думаю о двух моментах, которые Proxy действительно не так хорош, какObject.definePropertyи декоратор.

1. Совместимость
2. Гибкость

Следовать за:

  1. Поддерживает прямой захват всего класса
  2. Исправить связанные проблемы с утилитой
  3. Отдельный код и примеры, упакованные в виде библиотеки
  4. попробуй использоватьProxyвыполнить

Библиотеки со схожим функционалом

Только методы захвата, обработка относительно элементарна

То же

пройти черезAsyncFunctionСуждение, обеспечивающее возвращаемое значение по умолчанию после сбоя.

В основном захватывают асинхронные методы, принцип — загрузчик веб-пакетов, проходящий через AST. разноеasync-catch-loader,babel-plugin-promise-catcherПринцип аналогичен.

напиши в конце

Писать нелегко, если вы думаете, что это хорошо, лайк и комментарий — моя самая большая мотивация.

babel-plugin-proposal-class-properties)
setpublicclassfields