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