iT邦幫忙

2024 iThome 鐵人賽

DAY 23
0
Mobile Development

30天 使用chatGPT輔助學習APP完成接案任務委託系列 第 23

[Day23] React native call api 後發生了什麼事?架構設計與流程說明

  • 分享至 

  • xImage
  •  

今天我們要來講一下關於 redux action app 架構設計。我們專案使用的 redux 工具是 redux-toolkit

按下送出後發生了什麼事?

我們來從使用者更新資料按下送出到資料更新成功這個流程開始說明這段的呼叫 api 發生了什麼事,來了解 redux 流程內容。

如果要說的最簡單就是,呼叫更新資料 api user/updteInfo 然後取得回應資料。但這麼簡單嗎?因為我們使用了 Redux來管理我們的 api call 的資料。 ^lGLpL6Oz

流程

我們來說明一下流程的部分,點擊後會發生的流程如下。

  1. 使用者點擊更新按鈕: 使用者在 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;

  1. Redux Thunk 中間件: 使用 createAsyncThunk 來處理異步操作,這會在中間件中攔截 action。

  2. 使用 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);
    }
  },
);
  1. API 回應: 伺服器回應 API 請求,可能是成功或失敗。

  2. 更新 Redux 中的使用者狀態: 如果 API 請求成功,更新 Redux 中的使用者狀態。

  3. 處理錯誤,顯示錯誤訊息: 如果 API 請求失敗,處理錯誤(例如顯示錯誤訊息)。

  4. 更新 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

主要負責將 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;

Axios Config

在昨天我們有說過,需要把 token都放在 Header一起呼叫,但不想每一次 api call都寫一次。所以我們放在 axios設定這邊。

1. API Endpoint 定義

export const API_ENDPOINT = Config.API_ENDPOINT;`

這裡將API的基礎URL從React Native的環境變量中讀取出來,並儲存在API_ENDPOINT中,以便其他部分使用。這種方式允許根據不同的環境(開發、測試、正式環境)動態配置API的URL。

2. Token 獲取

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無效。

3. Axios 實例的創建

const instance: AxiosInstance & {
  setAuthenticationErrorHandler?: (handler: () => void) => void;
} = axios.create({
  baseURL: API_ENDPOINT,
  timeout: 600000,
});

這裡通過axios.create方法創建了一個AxiosInstance,設置了API的base URL以及請求的超時時間為600秒。這個instance將被用於所有的API請求,並可以根據需要進行配置。

4. Request攔截器

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,實現身份驗證。

5. Response攔截器

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

這在上面提到的 action 發起的中間件(Thunk)。 發起後的 action 會在中間呼叫 api文件。

userAction.ts

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,來對應異步操作的不同階段:

  • pending: 當異步操作開始執行時會觸發,通常用來設置 loading 狀態。
  • fulfilled: 當 Promise 成功時觸發,並將成功的結果傳遞到 Redux state。
  • rejected: 當 Promise 失敗時觸發,用來處理錯誤信息。

這些 action 分別對應於異步操作的三個狀態:進行中、成功和失敗。你可以在 reducer 中根據這些狀態來更新應用的狀態(例如顯示加載動畫、存儲數據或顯示錯誤信息)。

Slice

我們使用了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鐵人


上一篇
[Day22] React Native APP Token 認證相關
下一篇
[Day24] 表單設計:用 NativeBase 重現互動式 PDF 表單
系列文
30天 使用chatGPT輔助學習APP完成接案任務委託30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言