使用 React 和 Redux 进行集中错误处理
介绍
解决现代 Web 应用中的错误并非易事。由于同一应用中有多个复杂的活动部件,错误可能来自多个来源。其中,响应用户输入而发生的错误至关重要。为了获得正确的用户体验,应以人性化的方式将错误传达给最终用户,而不会破坏整个应用。
在常规的 React 和 Redux 应用中,由于 Redux 操作和 Reducer 的复杂性,处理错误乍一看可能是一项繁琐的任务。然而,利用 Redux 的特性,可以设计出一个集中的错误处理机制。在本指南中,我们探讨了在尽量减少返工的同时,集中处理 API 引发的错误的想法。然后,我们扩展了该方法以处理手动触发的错误,从而呈现一个成熟的错误系统。
设计
首先,让我们讨论一下在我们的 Web 应用中实施集中式错误处理系统的关键期望。虽然应用程序的要求可能有所不同,但以下是我们作为工程师经常遇到的一些任务。
将错误代码转换为人类可读的消息
当我们的应用涉及定期与用户输入交互的后端时,通常会在后端定义一组特定于平台的错误。例如,ToDo 应用的后端会有UnknownTodoID、TodoStateError、DuplicateToDo等错误。这些错误不属于标准的 HTTP 错误类型集,并且是特定于平台的。因此,后端必须有意义地将错误传达给最终用户。虽然在前端应用中使用硬编码映射为错误 ID 指定错误消息对于较小的应用有效,但随着复杂性的增加,错误消息最好来自后端本身。我们前端的错误处理程序应该能够从后端捕获错误和错误消息并将其显示给用户。
单点处理
处理错误时,应该有一个中央控制点。例如,您稍后可能会决定所有错误都应向日志服务器触发错误事件,以供进一步分析。如果错误直接从 API 调用传递到错误状态,我们最终会改变所有这些错误起源点。(或者,错误存储的侦听器可以在 Redux 上下文中工作,但这可能会严重降低性能。)
减少未来工作
在开发数百个 API 提取时,我们始终希望错误处理代码尽可能自动化。在理想情况下,我们不应该明确检查 API 响应对象中的错误。相反,处理机制应该使用响应的内容来找出错误。
考虑到上述要求,我们现在可以为集中式错误处理程序设计一个方案。虽然解决同一问题的设计方法有很多,但以下是我在应用程序使用 Redux 存储时使用的一种可用于生产环境的方法。
- 首先,确定您期望从数据源获得的响应对象的形状。
- 构建 API 交互点,以确保响应对象和预期形状相符。
- 创建一个错误减少器,观察应用程序中发生的任何错误并将其注册在错误状态中。
- 创建一个使用错误操作手动触发错误的结构。
- 最后,创建可重复使用的组件来显示错误。
响应对象形状
在设计 API 时,有几种设计响应对象的思路,每种思路都适用于一组特定的用例。在本指南中,我们将使用一个简单的对象,该对象适用于在前端和后端之间定期交换小数据部分的应用程序。以下是响应的 JSON 表示。
{
"status": "ok" or "error",
"data": [] or {},
"messages": [],
"errors": []
}
在上述结构中:
- data是 API 实际响应数据所在的位置。它可以是数组或单个对象。
- 消息是一个字符串数组。它由人类可读的字符串组成,指示成功请求后发生了什么。
- errors是一个字符串数组。它由可读的字符串组成,说明请求中出现了什么错误。
- status实际上是可选字段。为方便起见,如果出现问题,则可能是“error”,否则可能是“ok”。这不是 HTTP 状态或代码。
为了简化前端的错误处理代码,我们将使用上述结构的精简版本,其中消息和错误只能包含一个字符串。
{
"status": "ok" or "error",
"data": [] or {},
"message": string,
"error": string
}
因此,有了上述结构,我们现在知道在响应中可以找到错误消息的确切位置。但是,如果数据源失控怎么办?例如,我们可能正在联系第三方 API 来收集货币汇率,并且我们无法规定响应对象的外观。为此,我们需要响应转换。
构建 API 调用
为了给我们虚构的 ToDo 应用提供一些背景信息,让我们在 Todo 类型和操作中添加一些代码。请注意成功和错误操作负载的结构。它们始终包含数据或错误字段。
// todoTypes.js
export const GET_TODO_REQUEST = "GET_TODO_REQUEST";
export const GET_TODO_SUCCESS = "GET_TODO_SUCCESS";
export const GET_TODO_ERROR = "GET_TODO_ERROR";
// todoActions.js
export function loadTodoRequest(){
return {
type: GET_TODO_REQUEST
}
}
export function loadTodoSuccess(results){
return {
type: GET_TODO_SUCCESS,
data: results,
error: null
}
}
export function loadTodoError(error){
return {
type: GET_TODO_SUCCESS,
data: null,
error: error
}
}
让我们创建第一个 API 调用。在下面的代码中,我们假设响应的结构正确,如上所示。
// API call when the response shape is correct
import axios from 'axios';
export const loadTodos = () => {
return async function(dispatch) {
dispatch(loadTodoRequest());
try{
let response = (await exios.get("http://yourapi.com/todo/all")).data;
if(response.status == "ok"){
// check if the internal status is ok
// then pass on the data
dispatch(loadTodoSuccess(response.data));
}else{
// if internally there are errors
// pass on the error, in a correct implementation
// such errors should throw an HTTP 4xx or 5xx error
// so that it directs straight to the catch block
dispatch(loadTodoError(response.error));
}
}catch(error){
// any HTTP error is caught here
// can extend this implementation to customiz the error messages
// ex: dispatch(loadTodoError("Sorry can't talk to our servers right now"));
dispatch(loadTodoError(response.error));
}
}
}
如上所示,处理响应的唯一要求是我们需要将数据和错误重定向到实际的相应操作。请注意,我们可以进一步概括实现以直接接受响应对象,并且无需在 API 调用函数中单独定义错误字段和数据字段。但这可能是一个潜在的安全风险。
如果是外部 API 调用,唯一需要修改的是判断是否发生错误的逻辑。以下代码演示了一个示例情况。
export const loadRates = () => {
return async function(dispatch) {
dispatch(loadRatesRequest());
try{
let response = (await exios.get("http://rates.com/all")).data;
// has some error looks for known error patterns in return data
if(hasSomeError(response.data)){
dispatch(loadRatesError(response.error));
}else{
dispatch(loadRatesSuccess(response.error));
}
}catch(error){
dispatch(loadRatesError(response.error));
}
}
}
现在错误来源已经修复,让我们看看我们提交给不同错误操作的这些错误实际上是如何被错误减少器捕获的。
错误减少器
事实上,集中错误处理的整个魔力都发生在错误 Reducer 中。目前的问题是,有数百个错误操作(loadTodoError、loadUserError、loadRatesError 等)由各自的 API 调用触发。我们如何创建一个统一的错误 Reducer,让它监听所有这些操作,而不必在每次定义新的错误操作时都进行更新?我们使用了一个巧妙的技巧,或者说是 Redux Reducer 中的固有属性。让我们首先看看 Reducer 代码。
// errorReducer.js
const initState = {
error: null
};
export function errorReducer(state = initState, action){
const { error } = action;
if(error){
return {
error: error
}
}
return state;
}
就是这样!
它为什么有效?
如果您想知道为什么上述代码段会起作用,这很简单。在大多数日常需求中,我们习惯于每个操作一个 Reducer 的模式,因此我们往往会忽略这样一个事实:Reducer 只是条件检查,不会将操作限制为一个。例如,通过检查操作类型,Reducer 选择了某个操作,并不意味着它是选择相同操作的唯一 Reducer。Redux 将继续将触发的操作传递给每个可用的 Reducer,以查看可能的匹配项。
在我们的例子中,使用此属性,我们有一个错误减少器,用于检查操作的有效负载中是否存在非空错误字段。如果存在,它只需选择操作并捕获错误消息。这样就无需明确定义中央错误减少器需要捕获的操作类型。
注意:这将为好奇的人们打开更多的可能性。使用相同的方法,可以扩展相同的代码来创建一个集中式通知处理程序,该处理程序不仅可以捕获和显示错误,还可以捕获和显示所有不同类型的通知。
现在,您可以尝试一下代码,并检查它是否按预期工作。触发一个带有错误的 API 调用,并观察错误减少器的状态如何正确捕获它。接下来,我们需要能够手动显示错误。
手动触发错误
虽然 API 错误是最大的麻烦,但有时我们需要向用户明确显示错误。例如,在表单提交中,我们可以利用相同的错误状态来向用户反馈验证错误。为此,我们需要错误操作。
// errorTypes.js
export const SET_ERROR = "SET_ERROR";
// errorActions.js
export function setError(error){
return {
type: SET_ERROR,
error: error
}
}
在任何情况下,要设置错误,您只需调用上述操作,它就会按预期工作。现在我们已经弄清楚了错误处理,最后一步是向用户显示错误,以便他们获得适当的反馈。
显示错误
对于任何 Web 应用来说,显示错误通常都是独一无二的。为简单起见,让我们创建一个ErrorNotification,将其添加到应用组件层次结构的最顶层。该组件的操作很简单:如果错误存储中存在错误,它会显示一条消息,如果用户忽略错误,它会清除存储。我们需要修改错误减少器和操作以适应新的 UI 方面。
// errorTypes.js
export const SET_ERROR = "SET_ERROR";
export const HIDE_ERROR = "HIDE_ERROR";
// errorActions.js
export function setError(error){
return {
type: SET_ERROR,
error: error
}
}
export function hideError(){
return {
type: HIDE_ERROR
}
}
// errorReducer.js
const initState = {
error: null,
isOpen: false
};
export function errorReducer(state = initState, action){
const { error } = action;
if(error){
return {
error: error,
isOpen: true
}
}else if(action.type === HIDE_ERROR){
return {
error: null,
isOpen: false
}
}
return state;
}
// ErrorNotification.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
const ErrorNotification = (props) => {
const isOpen = useSelector(<span class="hljs-functi
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~