今天我們要來講一下關於 redux action app 架構設計。我們專案使用的 redux 工具是 redux-toolkit
我們來從使用者更新資料按下送出到資料更新成功這個流程開始說明這段的呼叫 api 發生了什麼事,來了解 redux 流程內容。
如果要說的最簡單就是,呼叫更新資料 api user/updteInfo
然後取得回應資料。但這麼簡單嗎?因為我們使用了 Redux來管理我們的 api call 的資料。 ^lGLpL6Oz
我們來說明一下流程的部分,點擊後會發生的流程如下。
使用者點擊更新按鈕: 使用者在 UI 上點擊送出按鈕,觸發更新操作。這個操作會 發送 updateUserInfo Redux Action: 這個操作會觸發一個 Redux action,通常是透過 dispatch 來呼叫 updateUserInfo。
通常這個發起動作的函數會寫在頁面上面,像是 使用者資訊(UserInfoPage)的頁面的 handleUpdate 函數
UserInfoPage.tsx
const UserInfoPage = () => {
const dispatch = useDispatch();
...
const handleUpdate = async () => {
try {
await dispatch(
updateUserInfo({
userId: user.userId,
data: userInfo,
}),
).unwrap();
} catch (error) {}
};
return (
<View style={styles.centered}>
<VStack space={4} width="90%">
....
<Button onPress={handleUpdate}>Submit</Button>
</VStack>
</View>
);
};
export default UserInfoPage;
Redux Thunk 中間件: 使用 createAsyncThunk 來處理異步操作,這會在中間件中攔截 action。
使用 Axios 發送更新使用者資料的 API 請求: 在中間件中,使用 Axios 發送 API 請求到伺服器,更新使用者資料。
export const updateUserInfo = createAsyncThunk(
'user/updateUserInfo',
async (
{
userId,
data,
}: {
userId: string;
data: {username: string; email: string; account: string};
},
thunkAPI,
) => {
try {
const response = await userApi.updateUserInfo(userId, data); <<<呼叫 axios
return response.data.user;
} catch (error: unknown) {
const errorMessage = (error as ErrorResponse)?.message || '未知錯誤';
return thunkAPI.rejectWithValue(errorMessage);
}
},
);
API 回應: 伺服器回應 API 請求,可能是成功或失敗。
更新 Redux 中的使用者狀態: 如果 API 請求成功,更新 Redux 中的使用者狀態。
處理錯誤,顯示錯誤訊息: 如果 API 請求失敗,處理錯誤(例如顯示錯誤訊息)。
更新 UI,顯示成功訊息: 根據 Redux 狀態的變化,更新 UI,並顯示成功訊息。
我們今天要來談的是架構的設計的部分。理解了上面流程,那程式碼要怎麼實作會比較好呢。
我們先從檔案目錄結構來說,因為其實在寫程式的時候我們會把檔案拆得細一點,職責簡單一點。
下面是我們設計的檔案目錄結構:
project/
│
├── src/
│ ├── api/ ---------------------------------> 管理api router的位置
│ │ └── userApi.ts
│ ├── lib/
│ │ └── configAxios.ts --------------> 設置 Axios 設定的檔案
│ ├── redux/ ------------------------------> 管理api router的位置
│ │ ├── actions/
│ │ │ └── userActions.ts ----------> action
│ │ ├── slice/
│ │ │ └── userSlice.ts ---------------> 管理 reduce state狀態
│ │ └── store.ts
│ ├── components/
│ │ └── UserInfoPage.tsx
│ └── App.tsx
│
├── package.json
└── tsconfig.json
主要負責將 api 的方法(Get,Post...)與 router定義清楚。同時也接口(interface)定義好。 ^pYGZatQM
import api from '../lib/configAxios';
import {API_ENDPOINT} from '../lib/configAxios';
const prefix = 'auth';
const userApi = {
registerUser(data: {
username: string;
password: string;
email: string;
account: string;
}) {
return api.post(`${API_ENDPOINT}/${prefix}/register`, data);
},
loginUser(data: {username: string; password: string}) {
return api.post(`${API_ENDPOINT}/${prefix}/login`, data);
},
getUserInfo(userId: string) {
return api.get(`${API_ENDPOINT}/users/${userId}`);
},
updateUserInfo(
userId: string,
data: {username: string; email: string; account: string},
) {
return api.put(`${API_ENDPOINT}/users/${userId}`, data);
},
};
export default userApi;
在昨天我們有說過,需要把 token都放在 Header一起呼叫,但不想每一次 api call都寫一次。所以我們放在 axios設定這邊。
export const API_ENDPOINT = Config.API_ENDPOINT;`
這裡將API的基礎URL從React Native的環境變量中讀取出來,並儲存在API_ENDPOINT
中,以便其他部分使用。這種方式允許根據不同的環境(開發、測試、正式環境)動態配置API的URL。
const getToken = async () => {
try {
const userToken = await AsyncStorage.getItem('userToken');
return userToken ? userToken : null;
} catch (error) {
console.error('Failed to retrieve token:', error);
return null;
}
};
這個函數從React Native的AsyncStorage
中異步獲取存儲的用戶token。這裡處理了可能的錯誤並返回null
,如果找不到token,這意味著用戶未登錄或token無效。
const instance: AxiosInstance & {
setAuthenticationErrorHandler?: (handler: () => void) => void;
} = axios.create({
baseURL: API_ENDPOINT,
timeout: 600000,
});
這裡通過axios.create
方法創建了一個AxiosInstance
,設置了API的base URL以及請求的超時時間為600秒。這個instance
將被用於所有的API請求,並可以根據需要進行配置。
instance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const token = await getToken();
if (token) {
config.headers = new AxiosHeaders({
...config.headers,
Authorization: `Bearer ${token}`,
});
}
return config;
},
error => {
return Promise.reject(error);
},
);
這段代碼設置了一個request
攔截器,目的是在每次發送API請求之前,從AsyncStorage
中讀取token,並將token自動添加到Authorization
標頭中。這樣的設計可以確保每次發送API請求時,都會攜帶用戶的JWT,實現身份驗證。
instance.interceptors.response.use(
response => {
return response;
},
error => {
const data = error.response?.data || {};
return Promise.reject(data);
},
);
這裡設置了一個response
攔截器,用來處理API響應中的錯誤。攔截器捕捉到響應中的錯誤後,將其錯誤數據返回。這樣的設計有助於處理API錯誤,例如返回錯誤信息給應用的其他部分來顯示給用戶。
import axios, {
AxiosInstance,
InternalAxiosRequestConfig,
AxiosHeaders,
} from 'axios';
import Config from 'react-native-config';
import AsyncStorage from '@react-native-community/async-storage';
export const API_ENDPOINT = Config.API_ENDPOINT;
const getToken = async () => {
try {
const userToken = await AsyncStorage.getItem('userToken');
return userToken ? userToken : null;
} catch (error) {
console.error('Failed to retrieve token:', error);
return null;
}
};
const instance: AxiosInstance & {
setAuthenticationErrorHandler?: (handler: () => void) => void;
} = axios.create({
baseURL: API_ENDPOINT,
timeout: 600000,
});
instance.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const token = await getToken();
if (token) {
config.headers = new AxiosHeaders({
...config.headers,
Authorization: `Bearer ${token}`,
});
}
return config;
},
error => {
return Promise.reject(error);
},
);
instance.interceptors.response.use(
response => {
return response;
},
error => {
const data = error.response?.data || {};
return Promise.reject(data);
},
);
export default instance;
這在上面提到的 action 發起的中間件(Thunk)。 發起後的 action 會在中間呼叫 api文件。
export const updateUserInfo = createAsyncThunk(
'user/updateUserInfo',
async (
{
userId,
data,
}: {
userId: string;
data: {username: string; email: string; account: string};
},
thunkAPI,
) => {
try {
const response = await userApi.updateUserInfo(userId, data);
return response.data.user;
} catch (error: unknown) {
const errorMessage = (error as ErrorResponse)?.message || '未知錯誤';
return thunkAPI.rejectWithValue(errorMessage);
}
},
);
// 這將生成三個 action:
// 1. user/updateUserInfo/pending - 當請求開始時
// 2. user/updateUserInfo/fulfilled - 當請求成功時
// 3. user/updateUserInfo/rejected - 當請求失敗時
createAsyncThunk
是 Redux Toolkit 中的一個強大的工具,主要用來處理 異步邏輯,例如 API 請求。它的工作原理基於 Promise,能夠在 Redux 中輕鬆管理 異步狀態(如請求進行中、成功、失敗等),並自動生成對應的 action 和 reducer。
自動生成三個 action:
當你定義一個 createAsyncThunk
,它會自動生成以下三種 action,來對應異步操作的不同階段:
loading
狀態。這些 action 分別對應於異步操作的三個狀態:進行中、成功和失敗。你可以在 reducer 中根據這些狀態來更新應用的狀態(例如顯示加載動畫、存儲數據或顯示錯誤信息)。
我們使用了Redux Toolkit的createSlice
來定義一個專門管理用戶狀態的Slice。這個 slice 幫我們處了 reduce 的狀態管理,還有從 action 來的狀態的 type (pending, fulfilled, rejected) 來決定要把 state改成什麼樣子。
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUserData: (state, action: PayloadAction<UserState>) => {
state.username = action.payload.username;
state.userId = action.payload.userId;
state.email = action.payload.email;
},
},
extraReducers: builder => {
builder
.addCase(registerUser.pending, state => {
state.status = 'loading';
})
.addCase(registerUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.username = action.payload.username;
state.email = action.payload.email;
})
.addCase(registerUser.rejected, (state, action: any) => {
state.status = 'failed';
state.error = action.payload.message;
})
.addCase(loginUser.pending, state => {
state.status = 'loading';
})
.addCase(loginUser.fulfilled, (state, action) => {
state.status = 'succeeded';
state.userId = action.payload.userId;
state.email = action.payload.email;
state.username = action.payload.username;
})
.addCase(getUserInfo.pending, state => {
state.status = 'loading';
})
.addCase(getUserInfo.fulfilled, (state, action) => {
state.status = 'succeeded';
state.username = action.payload.username;
state.email = action.payload.email;
});
},
});
我覺得 api redux 這邊會有點難懂,我其實以前學的時候也做了好久都搞不太懂。但是我知道只要複製貼上照個格式做就會動了😅。其實有的時候就是多做就會知道了。
不過其實我也覺得說的不是很清楚,要把概念講清楚真的很難。不過明天開始會開始加速,可能就概念講清楚一點,程式碼貼上去這樣。
#it鐵人