本文主要分为如下三个部分:html
本文的案例使用的技术栈包括: React
,Redux
,TypeScript
,Axios
,Lodash
。前端
HTTP 请求错误一般能够归为如下几类:react
服务器有响应,表示服务器响应了,而且返回了相应的错误信息
。ios
若是你不指望每个请求都显示服务器返回的特定错误信息,还能够根据 HTTP Status Code 对错误信息进一步归类:redux
4xx客户端错误: 表示客户端发生了错误,妨碍了服务器的处理
。好比:跨域
5xx服务器错误: 表示服务器没法完成合法的请求
。多是服务器在处理请求的过程当中有错误或者异常状态发生。好比:服务器
服务器无响应,表示请求发起了,可是服务器没有响应
。网络
这种状况多是由于网络故障(无网/弱网),或着跨域请求被拒绝(生产环境一般不会有跨域的状况出现,因此这个错误通常不用考虑)。若是你使用的 HTTP Client 没有返回错误信息,能够考虑显示一个通用错误信息(General Error Message)。函数
一般是因为 JS 代码编写错误,致使 JavaScript 引擎没法正确执行,从而报错。这一类错误在生产环境通常不会出现,所以能够根据业务需求决定是否处理这一类错误。常见的有:测试
应用中根据业务需求而 Throw 的 Error。
在上面的章节中咱们已经对应用中的 Error 进行了分类。 利用 Redux 咱们能够对 HTTP Request Error 进行统一的处理。
在进行 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); }; };
将 HTTP 请求的失败状态转化成 RequestFailedAction 以后,咱们须要写一个 Middleware 来处理它。
这里有人可能会问了,既然已经有 RequestFailedAction 了,还须要 Middleware 吗?能不能直接在 Reducer 中去处理它?其实也是能够的。可是写在 Reducer 里面,同一个 Action 修改了多个 State 节点,会致使代码耦合度增长,因此在这里咱们仍是使用 Middleware 的方式来处理。思路以下:
addNotificationAction
中。在这里咱们并不须要将全部的错误信息都存起来,由于 UI 只关心 Error 的类型和信息。createNotification
函数,生成一个带有 UUID 的 Notification,以便删除时使用。由于 notification 可能不止一个。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。
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 {}; }, }, {}, );
这一步就很简单了,从 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);
由于 Notification 是一个通用的组件,因此咱们通常会把它放到根组件 (Root) 上。
<WithNotifications> {({ notifications }) => ( <> {map(notifications, (notification: { type: string; msg: string }, id: number) => { return ( <div> {notification.msg} // 将你的 Notification 组件放到这里 {id} // 你能够用 id 去删除对应的 Notification </div> ); })} </> )} </WithNotifications>
固然,并非全部的 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); }; }; };
在测试 errorMiddleware 的时候,可能会遇到一个问题,就是咱们的 Notification 是根据一个以时间戳为 key 的对象,时间戳是根据当前时间生成的,每次跑测试时都会发生变化,如何解决呢?Mock getTime 方法就好啦。以下:
beforeEach(() => { class MockDate { getTime() { return 123456; } } global.Date = MockDate as any; }); afterEach(() => { global.Date = Date; });
利用 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 组件来讲,它只会捕获在它之下的组件,它不会捕获自身组件内部的错误。