Этот проект является интерфейсной частью стабильного проекта группы доходов от облачной музыки Автор этой статьиЧжан Вэйдун, другие участники проектаЧжао Сянтао
Кровавый случай, вызванный ошибкой
Известный корейский бойз-бэнд ранее размещал на нашей платформе цифровой альбом-блокбастер, изначально это было хорошо, но после того, как его положили на прилавки, посыпались жалобы. Некоторые страницы с отзывами пользователей вылетают при открытии.После экстренного расследования выяснилось, что настоящим виновником является следующий код.
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
Могут быть перехвачены только ошибки рендеринга дочерних компонентов, и существуют определенные ограничения.Следующие ситуации не могут быть обработаны:
- обработчик события(например, onClick, onMouseEnter)
- асинхронный код(например, requestAnimationFrame, setTimeout, обещание)
- рендеринг на стороне сервера
- 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
После того, как мы столкнулись с дилеммой апелляции, наша идея состоит в том, чтобы автоматически обернуть компонент обработки ошибок в подкомпонент с помощью скаффолдинга. Каркас дизайна выглядит следующим образом:
Вкратце шаги следующие:
-
Определите, является ли это версией React 16
-
прочитать файл конфигурации
-
Проверьте, было ли оно упаковано
ErrorBoundary
компоненты. Если нет, пройдите процесс исправления. Если да, то согласноforce
Этикетка определяет необходимость переупаковки. -
Возьмем процесс компонента пакета (процесс исправления на рисунке):
а. Сначала введите компонент обработки ошибок
б. Для подкомпонентов
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
Сделал пакет:
- Сначала определите, является ли это сервером
is_server
:
function is_server() {
return !(typeof window !== "undefined" && window.document);
}
- пакет
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, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!