Перехватывать исключения React

React.js
Перехватывать исключения React

Этот проект является интерфейсной частью стабильного проекта группы доходов от облачной музыки Автор этой статьиЧжан Вэйдун, другие участники проектаЧжао Сянтао

Кровавый случай, вызванный ошибкой

Известный корейский бойз-бэнд ранее размещал на нашей платформе цифровой альбом-блокбастер, изначально это было хорошо, но после того, как его положили на прилавки, посыпались жалобы. Некоторые страницы с отзывами пользователей вылетают при открытии.После экстренного расследования выяснилось, что настоящим виновником является следующий код.

  render() {
     const { data, isCreator, canSignOut, canSignIn } = this.props;
     const {  supportCard, creator, fansList, visitorId, memberCount } = data;
     let getUserIcon = (obj) => {
         if (obj.userType == 4) {
             return (<i className="icn u-svg u-svg-yyr_sml" />);
         } else if (obj.authStatus == 1) {
             return (<i className="icn u-svg u-svg-vip_sml" />);
         } else if (obj.expertTags && creator.expertTags.length > 0) {
             return (<i className="icn u-svg u-svg-daren_sml" />);
         }
         return null;
     };
     ...
  }

эта линияif (obj.expertTags && creator.expertTags.length )внутриcreatorдолжно бытьobj, из-за скользкой руки, случайно написал неправильно.

Для приведенного выше случаяlintИнструмент не может обнаружить его, потому чтоcreatorтакже оказывается переменной, что является чистой логической ошибкой.

Позже мы срочно исправили баг и все успокоилось. Хотя все подошло к концу, в моем сердце продолжает звучать голосКак не допустить повторения этой аварии. Ошибку такого типа невозможно заблокировать, поэтому нам следует подумать о разработке восходящего механизма, который может изолировать этот тип ошибки и гарантировать, что вся страница не будет затронута, если некоторые компоненты страницы неверны.

Введение в ErrorBoundary

Начиная с React 16 была введена концепция Error Boundaries, которая улавливает егоПодсборкаОшибки, созданные в, запишите журнал ошибок и отобразите содержимое с пониженной версией, конкретноеАдрес официального сайта.

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed

Эта особенность проясняет наши глаза и поднимает настроение, словно видя свет во тьме. Однако исследования показали, чтоErrorBoundaryМогут быть перехвачены только ошибки рендеринга дочерних компонентов, и существуют определенные ограничения.Следующие ситуации не могут быть обработаны:

как создатьErrorBoundaryкомпоненты

покаReact.Componentдобавить компонентstatic getDerivedStateFromError()илиcomponentDidCatch()Вот и все. Первая выполняет обработку понижения при возникновении ошибки, а вторая функция в основном предназначена для ведения журнала.официальный кодследующим образом

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

можно увидетьgetDerivedStateFromErrorОтлавливать ошибки, возникающие в дочерних компонентах, устанавливатьhasErrorПеременная,renderВ функции ухудшенный пользовательский интерфейс отображается в соответствии со значением переменной.

До сих пор компонент ErrorBoundary был определен, просто оберните подкомпонент при его использовании, как показано ниже.

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Обычное использование границ ошибок.

Увидев использование границ ошибок, большинство команд будут следовать официальному использованию и напишутerrorBoundaryHOC, а затем оберните подкомпонент. Пример скретч-проекта ниже

export default errorBoundaryHOC('Blocks')(
    connect(
        mapStateToProps,
        mapDispatchToProps
    )(Blocks)
);

вBlocksявляется компонентом отображения пользовательского интерфейса,errorBoundaryHOCкомпонент обработки ошибок, Конкретный исходный код можно увидетьздесь

Дилемма общего использования

Вышеупомянутый метод обертываетerrorBoundaryHOC. Для вновь разработанного кода его использовать удобнее, а вот для существующего кода будут относительно большие проблемы.

Поскольку формат экспорта имеетразличный

export class ClassName {...}
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export * as name1 from …

Итак, если вы используете исходный кодerrorBoundaryHOCИнкапсуляция изменит исходную структуру кода.Также очень хлопотно удалить инкапсуляцию, если она больше не потребуется в будущем.Стоимость внедрения решения высока, что очень сложно.

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

Бронзовый век - BabelPlugin

После того, как мы столкнулись с дилеммой апелляции, наша идея состоит в том, чтобы автоматически обернуть компонент обработки ошибок в подкомпонент с помощью скаффолдинга. Каркас дизайна выглядит следующим образом:

Вкратце шаги следующие:

  1. Определите, является ли это версией React 16

  2. прочитать файл конфигурации

  3. Проверьте, было ли оно упакованоErrorBoundaryкомпоненты. Если нет, пройдите процесс исправления. Если да, то согласноforceЭтикетка определяет необходимость переупаковки.

  4. Возьмем процесс компонента пакета (процесс исправления на рисунке):

    а. Сначала введите компонент обработки ошибок

    б. Для подкомпонентовErrorBoundaryпакет

Файл конфигурации выглядит следующим образом (.catch-react-error-config.json):

{
  "sentinel": {
    "imports": "import ServerErrorBoundary from '$components/ServerErrorBoundary'",
    "errorHandleComponent": "ServerErrorBoundary",
    "filter": ["/actual/"]
  },
  "sourceDir": "test/fixtures/wrapCustomComponent"
}

Исходный код до патча:

import React, { Component } from "react";

class App extends Component {
  render() {
    return <CustomComponent />;
  }
}

Код после чтения патча файла конфигурации:

//isCatchReactError
import ServerErrorBoundary from "$components/ServerErrorBoundary";
import React, { Component } from "react";

class App extends Component {
  render() {
    return (
      <ServerErrorBoundary isCatchReactError>
        {<CustomComponent />}
      </ServerErrorBoundary>
    );
  }
}

Вы можете видеть больше головы

import ServerErrorBoundary from '$components/ServerErrorBoundary'

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

Этот план используетbabel plugin, Автоматически импортировать ErrorBoundary на этапе компиляции кода и пакетно упаковывать компоненты, основной код:

const babelTemplate = require("@babel/template");
const t = require("babel-types");

const visitor = {
  Program: {
    // 在文件头部导入 ErrorBoundary
    exit(path) {
      // string 代码转换为 AST
      const impstm = template.default.ast(
        "import ErrorBoundary from '$components/ErrorBoundary'"
      );
      path.node.body.unshift(impstm);
    }
  },
  /**
   * 包裹 return jsxElement
   * @param {*} path
   */
  ReturnStatement(path) {
    const parentFunc = path.getFunctionParent();
    const oldJsx = path.node.argument;
    if (
      !oldJsx ||
      ((!parentFunc.node.key || parentFunc.node.key.name !== "render") &&
        oldJsx.type !== "JSXElement")
    ) {
      return;
    }

    // 创建被 ErrorBoundary 包裹之后的组件树
    const openingElement = t.JSXOpeningElement(
      t.JSXIdentifier("ErrorBoundary")
    );
    const closingElement = t.JSXClosingElement(
      t.JSXIdentifier("ErrorBoundary")
    );
    const newJsx = t.JSXElement(openingElement, closingElement, oldJsx);

    // 插入新的 jxsElement, 并删除旧的
    let newReturnStm = t.returnStatement(newJsx);
    path.remove();
    path.parent.body.push(newReturnStm);
  }
};

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

Полная реализация кода GitHubздесь

Хотя этот метод реализует неправильный захват и решение «снизу вверх», он очень сложен и громоздок в использовании, необходимо настроить Webpack и.catch-react-error-config.jsonТакже работает строительные леса, эффект не является удовлетворительным.

Золотой век - Декоратор

После того, как вышеприведенное решение вышло, я долго не мог найти элегантного решения, то ли слишком сложное в использовании (babelplugin), то ли слишком много изменений исходного кода (HOC), есть ли более изящная реализация.

Итак, есть схема декоратора (Decorator).

Исходный код реализации схемы декоратора использует TypeScript. При его использовании требуется взаимодействие с подключаемым модулем Babel для преобразования в версию ES. Подробности см. в инструкциях ниже.

TS предоставляет фабрики декораторов, декораторы классов, декораторы методов, декораторы доступа, декораторы свойств, декораторы параметров и т. д. В сочетании с характеристиками проекта мы используем декораторы классов.

Знакомство с декораторами классов

Декораторы класса объявляются перед объявлением класса (сразу после объявления класса). Декораторы классов применяются к конструкторам классов и могут использоваться для мониторинга, изменения или замены определений классов.

Ниже приведен пример.

function SelfDriving(constructorFunction: Function) {
    console.log('-- decorator function invoked --');
    constructorFunction.prototype.selfDrivable = true;
}

@SelfDriving
class Car {
    private _make: string;
    constructor(make: string) {
        this._make = make;
    }
}
let car: Car = new Car("Nissan");
console.log(car);
console.log(`selfDriving: ${car['selfDrivable']}`);

output:

-- decorator function invoked --
Car { _make: 'Nissan' }
selfDriving: true

Приведенный выше код выполняется первымSelfDrivingфункция, то автомобиль также получаетselfDrivableАтрибуты.

Вы можете видеть, что Decorator по сути является функцией, и вы также можете использовать@+函数名Украсьте в других местах, таких как классы, методы и т. д. Декораторы могут изменять определения классов, получать динамические данные и т. д.

Полное руководство по декоратору TS см.Официальный учебник

Итак, наша схема захвата ошибок устроена следующим образом.

@catchreacterror()
class Test extends React.Component {
  render() {
    return <Button text="click me" />;
  }
}

catchreacterrorПараметры функцииErrorBoundaryкомпоненты, пользователи могут использовать пользовательскиеErrorBoundary, если не передано, используется значение по умолчаниюDefaultErrorBoundaryкомпоненты;

catchreacterrorОсновной код выглядит следующим образом:

import React, { Component, forwardRef } from "react";

const catchreacterror = (Boundary = DefaultErrorBoundary) => InnerComponent => {
  class WrapperComponent extends Component {
    render() {
      const { forwardedRef } = this.props;
      return (
        <Boundary>
          <InnerComponent {...this.props} ref={forwardedRef} />
        </Boundary>
      );
    }
  }
};

Возвращаемое значение представляет собой HOC, используяErrorBoundaryОберните дочерние компоненты.

Добавить захват ошибок рендеринга на стороне сервера

существуетпредставлятьВнутри официальный рендеринг, официальныйErrorBoundaryне поддерживается, поэтому для SSR мы используемtry/catchСделал пакет:

  1. Сначала определите, является ли это серверомis_server:
function is_server() {
  return !(typeof window !== "undefined" && window.document);
}
  1. пакет
if (is_server()) {
  const originalRender = InnerComponent.prototype.render;

  InnerComponent.prototype.render = function() {
    try {
      return originalRender.apply(this, arguments);
    } catch (error) {
      console.error(error);
      return <div>Something is Wrong</div>;
    }
  };
}

Наконец, сформировалосьcatch-react-errorЭта библиотека упрощает поиск ошибок React.

инструкции поймать-реагировать-ошибка

1. Установкаcatch-react-error

npm install catch-react-error

2. Установите плагин Babel ES7 Decorator.

npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-class-properties

добавить плагин бабеля

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

3. Импортировать поймать-реагировать-ошибку

import catchreacterror from "catch-react-error";

4. Используйте@catchreacterror Decorator

@catchreacterror()
class Test extends React.Component {
  render() {
    return <Button text="click me" />;
  }
}

catchreacterrorФункция принимает один параметр:ErrorBoundary(Если он не указан, он будет использоваться по умолчаниюDefaultErrorBoundary)

5. Используйте@catchreacterrorОбработка функционального компонента

выше дляClassComponentОднако некоторым людям нравится использовать функциональные компоненты, и здесь также представлены методы использования, как показано ниже.

const Content = (props, b, c) => {
  return <div>{props.x.length}</div>;
};

const SafeContent = catchreacterror(DefaultErrorBoundary)(Content);

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>这是正常展示内容</h1>
      </header>
      <SafeContent/>
    </div>
  );
}

6. Как создать свои собственные границы ошибок

см. вышекак создатьErrorBoundaryкомпоненты, а затем измените на то, что вам нужно, например, вcomponentDidCatchСообщить об ошибках и т.д.

Полный код GitHub находится здесьcatch-react-error.

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!