Post

在前端如何優雅地接 API

在現代前端開發中,接 API 是一個不可避免的事。要實現優雅且高效的 API 請求管理,我們需要考慮多個方面,包括攔截器、錯誤處理、加載狀態和請求取消等。這些最佳實踐不僅能提升應用的可維護性和可讀性,還能極大地改善使用者體驗。本篇文章將介紹如何在前端優雅地處理 API 請求

你可以參考之前的兩篇文章,了解更多有關 AJAX 和常見 HTTP 的請求方式:

如何優雅接 API?

選擇 Axios 替代 XHR 和 Fetch

選擇 Axios 取代 XHRfetch,因為 Axios 提供更方便的功能,例如:Base URL、Timeout、以及自動轉換 JSON 格式等等

設定 Base URL 避免重複

通常 Backend Server 都是同一個,可以設定 Base URL,就不用重複寫了

1
2
3
4
5
6
const apiClient = axios.create({
    baseURL: 'https://api.example.com',
});

// 也可以之後再修改 baseURL
apiClient.defaults.baseURL = 'https://other-domain.com/api/';

設置 Timeout 避免無限等待

沒有完美的網路,設定 Timeout,避免無限期等待 Request

1
2
3
const apiClient = axios.create({
    timeout: 3000,  // 3s
});

提供 Refresh 按鈕和 Retry 機制

沒有完美的網路,建議可以提供 refresh 按鈕讓使用者重試,也可以自己設計 retry 機制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function axiosWithRetry(config, retries = 3, backoff = 300) {
  try {
    const response = await axios(config);
    return response;
  } catch (error) {
    if (retries === 0) {
      throw error;
    } else {
      console.log(`Retrying... ${retries} attempts left.`);
      await new Promise(resolve => setTimeout(resolve, backoff));
      return axiosWithRetry(config, retries - 1, backoff * 2);
    }
  }
}

當然也可以用別人寫好的套件 axios-retry

1
2
3
4
5
import axiosRetry from 'axios-retry';

axiosRetry(apiClient, { retries: 3 });

apiClient.get('/test'); // 照常使用

通常 retry 會設定指數型的,指數型延遲通過逐漸增加重試間隔,減少了伺服器的瞬時負載

1
axiosRetry(axios, { retryDelay: axiosRetry.exponentialDelay });

Credentials 最佳實踐

如果後端是用 headertoken 來驗證登入狀態,可以使用攔截器,讓全部的 request 都會自動帶 header

1
2
3
4
5
6
7
apiClient.interceptors.request.use(config => {
    // 每一個 request 都帶上 token
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    return config;
}, error => {
    return Promise.reject(error);
});

如果後端驗證不是使用 header 的方式,而是使用 session cookie 的方式,需要多設定 withCredentials: true,這樣 cookie 才帶得到後端

1
2
3
const apiClient = axios.create({
    withCredentials: true,
});

給 UI 反饋添加 Loading 圖示

給 UI 反饋,例如 Loading 圖示或是進度條

使用 async / await 取代 then / catch

使用 async await 取代 Promise 的 .then.catch,語法更接近於同步程式碼,讓異步程式碼看起來更簡單和直觀

Error Handler

記得做 error handler 使用 try...catch 搭配 async await 可以統一處理錯誤,使程式碼更易於理解和維護

1
2
3
4
5
6
7
8
9
10
async function fetchData() {
    try {
        const response = await apiClient.get('/data');
        console.log(response.data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

fetchData();

可以利用攔截器,統一處理錯誤,例如彈出全域的 toast

image

1
2
3
4
5
6
apiClient.interceptors.response.use(response => {
    return response;
}, error => {
    showToast('Error: Internal Server Error');
    return Promise.reject(error);
});

取消重複的 API 請求

取消重複的請求:當多次觸發同一 API 時,可以取消前一個請求,避免重複數據處理和網絡資源浪費,或是後面發出去的 Request 先回來了,第一個發出去的 Request 才回來,導致髒資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
  <button v-on:click="fetchData">Fetch Data</button>
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

let controller = null;

async function fetchData() {
  if (controller) {
    controller.abort(); // 取消前一個請求
  }

  controller = new AbortController();

  try {
    const response = await apiClient.get('/data', {
      signal: controller.signal,
    });
    console.log(response.data);
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('Request canceled:', error.message);
    } else {
      console.error('API error:', error);
    }
  }
}
</script>

防抖和節流控制 API 請求頻率

對頻繁的 API 請求可以使用防抖(debounce),防止短時間內多次觸發 API 請求

  • 何時使用:適合處理在一段時間內多次觸發的事件,但只在最後一次觸發後執行。例如,輸入框的即時搜索功能
  • 原理:在事件停止觸發後的一段時間(如 300 毫秒)內,才執行事件處理函數。如果在這段時間內再次觸發事件,計時器重新計時

可以參考我的另一篇文章 去抖 Debounce & 節流 Throttle 優化前端效能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { debounce } from 'lodash-es';

async function _fetchData() {
    try {
        const response = await apiClient.get('/data');
        console.log(response.data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

const fetchData = debounce(_fetchData, 300); // 300ms 的防抖

fetchData();

禁用提交按鈕並顯示 Loading

可以 disable submit 按鈕,並顯示 Loading 來避免重複提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
    <button v-on:click="submitForm" :disabled="isSubmitting">
    <span v-if="isSubmitting">Loading...</span>
    <span v-else>Submit</span>
    </button>
</div>
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const isSubmitting = ref(false);

async function submitForm() {
    isSubmitting.value = true;
    try {
        await axios.post('/api/submit', { data: 'example' });
        alert("成功!")
    } catch (error) {
        alert("發生錯誤 QQ")
    } finally {
        isSubmitting.value = false;
    }
};
</script>

同時發送多個 API 請求

同時打兩個 API,可以善用 Promise.all(),避免原本要等第一個 request 回來,才發出下一個 request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function fetchData() {
    try {
        const [userResponse, postResponse] = await Promise.all([
            apiClient.get('/users'),
            apiClient.get('/posts')
        ]);
        console.log('Users:', userResponse.data);
        console.log('Posts:', postResponse.data);
    } catch (error) {
        console.error('錯誤', error);
    }
}

fetchData();

範例:在 Vue 3 優雅地接 API

安裝 Axios

首先,確保你已經安裝了 Axios:

1
npm install --save axios

創建 API Client

在 src 目錄下創建一個 apiClient.js 檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import axios from 'axios';

const apiClient = axios.create({
    baseURL: 'https://api.example.com', // 設定好 base url
    timeout: 5000, // 設定 timeout,避免無限期的等待
});

apiClient.interceptors.request.use(config => {
    // 每一個 request 都帶上 token
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    return config;
}, error => {
    return Promise.reject(error);
});

apiClient.interceptors.response.use(response => {
    return response;
}, error => {
    if (error.response.status === 401) {
        // 沒登入,跳轉到登入頁面
        window.location.href = '/login';
    } else if (error.response.status === 500) {
        // 伺服器錯誤,可以顯示全域的錯誤訊息
        alert('伺服器錯誤,請稍後再試');
    }
    return Promise.reject(error);
});

export default apiClient;

創建一個自定義 Hook

在 src 目錄下創建一個 useUserApi.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref } from 'vue';
import apiClient from './apiClient';

export function useUserApi() {
    const userData = ref(null);
    const loading = ref(false);
    const error = ref(null);

    const fetchUser = async (userId) => {
        loading.value = true;
        error.value = null;
        try {
            const response = await apiClient.get(`/users/${userId}`);
            userData.value = response.data;
        } catch (err) {
            error.value = err;
        } finally {
            // 記得 loading 不管成功或失敗都要 set 成 false,不然明明 API 失敗了還在 loading 畫面
            loading.value = false;
        }
    };

    return { userData, loading, error, fetchUser };
}

使用自定義 Hook

在你的 Vue 元件中使用自定義 Hook 來進行 API 請求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <div>
    <!-- 可以的話,提供使用者 retry 的機會 -->
    <button v-on:click="fetchUser(1)">Fetch User</button>
    <div v-if="loading">Loading...</div>
    <div v-if="error">Error: </div>
    <pre v-if="userData"></pre>
  </div>
</template>

<script setup>
import { useUserApi } from './useUserApi';

const { userData, loading, error, fetchUser } = useUserApi();
</script>
This post is licensed under CC BY 4.0 by the author.