本文主要分为以下三个部分:
- Error 的分类
- 分步骤详细讲解如何利用 Redux 统一处理 Error
- 错误信息的收集
本文的案例使用的技术栈包括: React
,Redux
,TypeScript
,Axios
,Lodash
。
Error 的分类
HTTP 请求错误
HTTP 请求错误通常可以归为以下几类:
服务器有响应的错误
服务器有响应,表示服务器响应了,并且返回了相应的错误信息
。
如果你不期望每一个请求都显示服务器返回的特定错误信息,还可以根据 HTTP Status Code 对错误信息进一步归类:
4xx客户端错误: 表示客户端发生了错误,妨碍了服务器的处理
。比如:- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 408 Request Timeout
- 409 Conflict
5xx服务器错误: 表示服务器无法完成合法的请求
。可能是服务器在处理请求的过程中有错误或者异常状态发生。比如:- 500 Internal Server Error
- 501 Not Implemented
- 503 Service Unavailable
服务器无响应的错误
服务器无响应,表示请求发起了,但是服务器没有响应
。
这种情况可能是因为网络故障(无网/弱网),或着跨域请求被拒绝(生产环境通常不会有跨域的情况出现,所以这个错误一般不用考虑)。如果你使用的 HTTP Client 没有返回错误信息,可以考虑显示一个通用错误信息(General Error Message)。
应用程序错误
代码错误
通常是由于 JS 代码编写错误,导致 JavaScript 引擎无法正确执行,从而报错。这一类错误在生产环境一般不会出现,因此可以根据业务需求决定是否处理这一类错误。常见的有:
- SyntaxError语法错误
- ReferenceError引用错误
- TypeError类型错误
Throw Error
应用中根据业务需求而 Throw 的 Error。
Redux 中的 Error 处理
在上面的章节中我们已经对应用中的 Error 进行了分类。 利用 Redux 我们可以对 HTTP Request Error 进行统一的处理。
Step1: 裂变 HTTP Request Action
在进行 HTTP 请求的时候,我们通常会发起一个 Action。如果将请求成功和失败的状态裂变成两个 Action,RequestSuccessAction
和 RequestFailedAction
,那么通过 RequestFailedAction,就能够对所有 HTTP 请求的错误进行统一处理。
requestMiddleware.ts
export const requestMiddleware: any = (client: AxiosInstance) => {
return ({ dispatch }: MiddlewareAPI<any>) =>
(next: Dispatch<any>) =>
(action: IRequestAction) => {
if (isRequestAction(action)) {
dispatch(createReqStartAction(action));
return client.request(action.payload)
.then((response: AxiosResponse) => {
return dispatch(createSuccessAction(action, response));
})
.catch((error: AxiosError) => {
return dispatch(createFailedAction(action, error, action.meta.omitError));
});
}
return next(action);
};
};
Step2: 创建 errorMiddleware,将 Error 转化为 Notification Action
将 HTTP 请求的失败状态转化成 RequestFailedAction 之后,我们需要写一个 Middleware 来处理它。
这里有人可能会问了,既然已经有 RequestFailedAction 了,还需要 Middleware 吗?能不能直接在 Reducer 中去处理它?其实也是可以的。但是写在 Reducer 里面,同一个 Action 修改了多个 State 节点,会导致代码耦合度增加,所以在这里我们还是使用 Middleware 的方式来处理。思路如下:
- 如果 Action 是一个 RequestFailedAction,那么根据错误的分类,将错误的类型和信息存储到
addNotificationAction
中。在这里我们并不需要将所有的错误信息都存起来,因为 UI 只关心 Error 的类型和信息。 - 根据 Error 的分类,Dispatch 带有不同 Error Type 和 Error Message 的 Action。
- 创建
createNotification
函数,生成一个带有 UUID 的 Notification,以便删除时使用。因为 notification 可能不止一个。 - 通过 Dispatch
removeNotificationAction
来移除 Notification。
export interface INotification {
[UUID: number]: {
type: string;
msg: string;
};
}
const createNotification = ({ type, msg }: { type: string; msg: string }): INotification => {
const id = new Date().getTime();
return {
[id]: {
type,
msg,
},
};
};
完整代码如下:
errorMiddleware.ts
import { AnyAction, Dispatch, MiddlewareAPI } from "redux";
import { isRequestFailedAction } from "../request";
import {
addNotification,
INotification,
} from "./notificationActions";
export enum ErrorMessages {
GENERAL_ERROR = "Something went wrong, please try again later!",
}
enum ErrorTypes {
GENERAL_ERROR = "GENERAL_ERROR",
}
export const createNotification = ({ type, msg }: { type: string; msg: string }): INotification => {
const id = new Date().getTime();
return {
[id]: {
type,
msg,
},
};
};
export const errorMiddleware = ({ dispatch }: MiddlewareAPI) => {
return (next: Dispatch<AnyAction>) => {
return (action: AnyAction) => {
if (isRequestFailedAction(action)) {
const error = action.payload;
if (error.response) {
dispatch(
addNotification(
createNotification({
type: error.response.error,
msg: error.response.data.message,
}),
),
);
} else {
dispatch(
addNotification(
createNotification({
type: ErrorTypes.GENERAL_ERROR,
msg: ErrorMessages.GENERAL_ERROR,
}),
),
);
}
}
return next(action);
};
};
};
notificationActions.ts
import { createAction } from "redux-actions";
export interface INotification {
[UUID: number]: {
type: string;
msg: string;
};
}
export const addNotification = createAction(
"@@notification/addNotification",
(notification: INotification) => notification,
);
export const removeNotification = createAction("@@notification/removeNotification", (id: number) => id);
export const clearNotifications = createAction("@@notification/clearNotifications");
服务器需要保证每一个 HTTP Reqeust 都有相应的 Error Message,不然前端就只能根据 4xx 或者 5xx 这种粗略的分类来显示 Error Message。
Step3: 处理 Notification Action
notificationsReducer.ts
import { omit } from "lodash";
import { Action, handleActions } from "redux-actions";
import { addNotification, clearNotifications, removeNotification } from "./notificationActions";
export const notificationsReducer = handleActions(
{
[`${addNotification}`]: (state, action: Action<any>) => {
return {
...state,
...action.payload,
};
},
[`${removeNotification}`]: (state, action: Action<any>) => {
return omit(state, action.payload);
},
[`${clearNotifications}`]: () => {
return {};
},
},
{},
);
Step4: 从 Store 中获取 Notification,并通过 React Child Render 提供给子组件。
这一步就很简单了,从 Store 中拿到 Notifications,然后通过 React Child Render 将它提供给子组件,子组件就可以根据它去显示 UI 了。
WithNotifications.tsx
import { isEmpty } from "lodash";
import * as React from "react";
import {
connect,
DispatchProp,
} from "react-redux";
import {
clearNotifications,
INotification,
} from "./notificationActions";
interface IWithNotificationsCoreInnerProps {
notifications: INotification;
}
interface IWithNotificationsCoreProps extends DispatchProp {
notifications: INotification;
children: (props: IWithNotificationsCoreInnerProps) => React.ReactNode;
}
class WithNotificationsCore extends React.Component<IWithNotificationsCoreProps> {
componentWillUnmount() {
this.props.dispatch(clearNotifications());
}
render() {
if (isEmpty(this.props.notifications)) {
return null;
}
return this.props.children({
notifications: this.props.notifications,
});
}
}
const mapStateToProps = (state: any) => {
return {
notifications: state.notifications,
};
};
export const WithNotifications = connect(mapStateToProps)(WithNotificationsCore);
Step5: 显示 Error Messages
因为 Notification 是一个通用的组件,所以我们一般会把它放到根组件 (Root) 上。
<WithNotifications>
{({ notifications }) => (
<>
{map(notifications, (notification: { type: string; msg: string }, id: number) => {
return (
<div>
{notification.msg} // 将你的 Notification 组件放到这里
{id} // 你可以用 id 去删除对应的 Notification
</div>
);
})}
</>
)}
</WithNotifications>
Step6: 添加白名单
当然,并不是所有的 API 请求出错我们都需要通知给用户。这时候你就需要加一个白名单了,如果在这个白名单内,则不将错误信息通知给用户。可以考虑在 Requst Action 的 Meta 中加一个 omitError
的 flag,当有这个 flag 的时候,则不进行通知。让我们修改一下 errorMiddleware,如下:
errorMiddleware.ts
export const errorMiddleware = ({ dispatch }: MiddlewareAPI) => {
return (next: Dispatch<AnyAction>) => {
return (action: AnyAction) => {
const shouldOmitError = get(action, "meta.omitError", false);
if (isRequestFailedAction(action) && !shouldOmitError) {
const error = action.payload;
if (error.response) {
// same as before
} else {
// same as before
}
return next(action);
};
};
};
Step7: 测试
在测试 errorMiddleware 的时候,可能会遇到一个问题,就是我们的 Notification 是根据一个以时间戳为 key 的对象,时间戳是根据当前时间生成的,每次跑测试时都会发生变化,如何解决呢?Mock getTime 方法就好啦。如下:
beforeEach(() => {
class MockDate {
getTime() {
return 123456;
}
}
global.Date = MockDate as any;
});
afterEach(() => {
global.Date = Date;
});
错误信息的收集
componentDidCatch
利用 React componentDidCatch
生命周期方法将错误信息收集到 Error Reporting 服务。这个方法有点像 JS 的 catch{}
,只不过是针对组件的。大多数时候我们希望 ErrorBoundary 组件贯穿我们的整个应用,所以一般会将它放在根节点上 (Root)。
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 组件来说,它只会捕获在它之下的组件,它不会捕获自身组件内部的错误。