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

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

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

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

предисловие

Никто не идеален, поэтому код всегда будет ошибаться, ошибки не страшны, главное как с ними справляться.
Я просто хочу спросить вас, как отловить ошибки реагирующего приложения? В настоящее время:

  • Xiaobai+++: Как с этим бороться?
  • Сяобай++: ErrorBoundary
  • Xiaobai+: ErrorBoundary, попробуйте поймать
  • Xiaohe#: ErrorBoundary, попробуйте поймать, window.onerror
  • 小黑##: Это серьезный вопрос, я знаю N способов справиться с ним, какое у вас есть лучшее решение?

ErrorBoundary

EerrorBoundary вышел в 16 версии.Кто-то спрашивал про 15 версию.Я ее не слушаю.Все равно пользуюсь 16.Конечно,в 15 она есть.unstable_handleError.

Введение на официальном сайте ErrorBoundary более подробное, дело не в этом, дело в том, какие исключения он может ловить.

  • Отрисовка дочерних компонентов
  • функция жизненного цикла
  • Конструктор
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>

Мир с открытым исходным кодом хорош, его уже упаковал великий богreact-error-boundaryТакая отличная библиотека.
Вам нужно заботиться только о том, о чем вам нужно заботиться после возникновения ошибки, и прийти сноваReset, Идеально.

import {ErrorBoundary} from 'react-error-boundary'

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

const ui = (
  <ErrorBoundary
    FallbackComponent={ErrorFallback}
    onReset={() => {
      // reset the state of your app so the error doesn't happen again
    }}
  >
    <ComponentThatMayError />
  </ErrorBoundary>
)

К сожалению, границы ошибок не улавливают эти ошибки:

  • обработчик события
  • Асинхронный код (например, обратные вызовы setTimeout или requestAnimationFrame)
  • Код рендеринга на стороне сервера
  • сами границы ошибки выдают ошибки

Оригинальный текст можно найти на официальном сайтеintroducing-error-boundaries

Цель этой статьи — отлов ошибок в обработчиках событий.
У чиновника тоже есть планhow-about-event-handlers, который пытается поймать.
Но, столько обработчиков событий, о боже, сколько писать. . . . . . . . . . . . . . . . . . . .

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

За границей ошибки

Давайте сначала посмотрим на таблицу, в которой перечислены средства и объем исключений, которые мы можем перехватывать.

тип исключения Синхронный метод асинхронный метод загрузка ресурсов Promise async/await
try/catch
window.onerror
error
unhandledrejection

try/catch

Может перехватывать синхронные и асинхронные/ожидающие исключения.

window.onerror , событие ошибки

    window.addEventListener('error', this.onError, true);
    window.onerror = this.onError

window.addEventListener('error')Это можно сравнить сwindow.onerrorМножественные исключения регистрации ресурсов. Обратите внимание, что последний параметрtrue, falseЭто может быть не так хорошо, как вы ожидаете.

Конечно, если у вас есть вопросы по поводу значения третьего параметра, мне до вас нет дела. до свидания.

unhandledrejection

Обратите внимание, что последний параметрtrue.

window.removeEventListener('unhandledrejection', this.onReject, true)

Он перехватывает исключение неперехваченного промиса.

XMLHttpRequest и выборка

XMLHttpRequestС ним очень легко обращаться, и у него есть собственное событие onerror. Конечно, вы не будете на 99,99% опираться на собственныеXMLHttpRequestупаковать библиотеку,axiosДействительно ароматный, есть этот полный механизм обработки ошибок.

Что касаетсяfetch, Беги с уловом сам, если не справишься, то это твоя проблема.

Так много, так тяжело.
К счастью, на самом деле есть библиотекаreact-error-catchЭто компонент, инкапсулированный на основе ErrorBoudary, error и unhandledrejection.

Суть его в следующем

   ErrorBoundary.prototype.componentDidMount = function () {
        // event catch
        window.addEventListener('error', this.catchError, true);
        // async code
        window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
    };

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

import ErrorCatch from 'react-error-catch'

const App = () => {
  return (
  <ErrorCatch
      app="react-catch"
      user="cxyuns"
      delay={5000}
      max={1}
      filters={[]}
      onCatch={(errors) => {
        console.log('报错咯');
        // 上报异常信息到后端,动态创建标签方式
        new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
      }}
    >
      <Main />
    </ErrorCatch>)
}

export default 

Хлопать, хлопать.

На самом деле это не так: самая важная ошибка, которую фиксирует ошибка, — это предоставление информации о стеке ошибок, что довольно недружественно для анализа ошибок, особенно после упаковки.

Так много ошибок, что я сначала позабочусь об обработчиках событий в React.
В остальном продолжение следует.

Перехват исключения обработчиком событий

Пример

Принцип моей идеи очень прост, используйтеdecoratorпереопределить исходный метод.

Первый взгляд на использование:


   @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }
        
        .......
        其他可能产生异常的代码
        .......
        
       Toast.success("创建订单成功");
    }

Обратите внимание на четыре параметра:

  • сообщение: При возникновении ошибки сообщение об ошибке печатается
  • тост: произошла ошибка, будь то тост
  • отчет: если есть ошибка, сообщите об этом
  • log: используйте console.error для печати

Может быть, вы сказали, что это, это, новости мертвы, необоснованны. Если бы у меня были другие новости.
В этот момент я слегка улыбнулся, не волнуйтесь, давайте посмотрим на кусок кода

  @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }
       
        .......
        其他可能产生异常的代码
        .......
        
       throw new CatchError("创建订单失败了,请联系管理员", {
           toast: true,
           report: true,
           log: false
       })
       
       Toast.success("创建订单成功");

    }

Да-да, это можно сделать, закинув кастомыCatchErrorчтобы переопределить предыдущие параметры по умолчанию.

этоmethodCatchОшибки, которые можно поймать, синхронные и асинхронные, давайте посмотрим на полный код.

определение типа

export interface CatchOptions {
    report?: boolean;
    message?: string;
    log?: boolean;
    toast?: boolean;
}

// 这里写到 const.ts更合理
export const DEFAULT_ERROR_CATCH_OPTIONS: CatchOptions = {
    report: true,
    message: "未知异常",
    log: true,
    toast: false
}

пользовательский CatchError

import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";

export class CatchError extends Error {

    public __type__ = "__CATCH_ERROR__";
    /**
     * 捕捉到的错误
     * @param message 消息
     * @options 其他参数
     */
    constructor(message: string, public options: CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {
        super(message);
    }
}

декоратор

import Toast from "@components/Toast";
import { CatchOptions, DEFAULT_ERROR_CATCH_OPTIONS } from "@typess/errorCatch";
import { CatchError } from "@util/error/CatchError";


const W_TYPES = ["string", "object"];
export function methodCatch(options: string | CatchOptions = DEFAULT_ERROR_CATCH_OPTIONS) {

    const type = typeof options;

    let opt: CatchOptions;

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

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

        const oldFn = descriptor.value;

        Object.defineProperty(descriptor, "value", {
            get() {
                async function proxy(...args: any[]) {
                    try {
                        const res = await oldFn.apply(this, args);
                        return res;
                    } catch (err) {
                        // if (err instanceof CatchError) {
                        if(err.__type__ == "__CATCH_ERROR__"){
                            err = err as CatchError;
                            const mOpt = { ...opt, ...(err.options || {}) };

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

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

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

                        } else {
                            
                            const message = err.message || opt.message;
                            console.error("asyncMethodCatch:", message, err);

                            if (opt.toast) {
                                Toast.error(message);
                            }
                        }
                    }
                }
                proxy._bound = true;
                return proxy;
            }
        })
        return descriptor;
    }
}

в заключении

  1. Используйте декоратор, чтобы переписать исходный метод для обнаружения ошибок.
  2. Настраивая класс ошибки и выбрасывая его, вы можете добиться переопределения параметров по умолчанию. Добавлена ​​гибкость.
  @methodCatch({ message: "创建订单失败", toast: true, report:true, log:true })
    async createOrder() {
        const data = {...};
        const res = await createOrder();
        if (!res || res.errCode !== 0) {
            return Toast.error("创建订单失败");
        }
       Toast.success("创建订单成功");
       
        .......
        其他可能产生异常的代码
        .......
        
       throw new CatchError("创建订单失败了,请联系管理员", {
           toast: true,
           report: true,
           log: false
       })
    }

Следующий шаг

Какой следующий шаг, сделай шаг и увидишь шаг.

Нет, дорога впереди еще очень длинная. Это базовая версия.

  1. Расширенные результаты для поддержки большего количества типов и версий хуков.

@XXXCatch
classs AAA{
    @YYYCatch
    method = ()=> {
    }
}
  1. абстрактный, абстрактный, абстрактный

Шутка окончена, давайте серьезно:

Проблемы с текущей программой:

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

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

Версия крючков

Некоторые землекопы сказали, что в этом возрасте, кто еще не использует Крючки.
Да, воротилы правы, надо идти в ногу со временем.
Базовая версия Hooks уже доступна, пожалуйста, сначала поделитесь ею и используйте ее, а последующие статьи последуют.

Имя крючка - useCatch


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, вы можете сначала посмотреть на код:

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;
}

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

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

error-boundaries
Реагировать на обработку исключений
catching-react-errors
Расширенный механизм обработки исключений React — границы ошибок
decorator
core-decorators
autobind.js