本文主要分爲如下三個部分: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 組件來講,它只會捕獲在它之下的組件,它不會捕獲自身組件內部的錯誤。