在前端如何優雅地接 API
在現代前端開發中,接 API 是一個不可避免的事。要實現優雅且高效的 API 請求管理,我們需要考慮多個方面,包括攔截器、錯誤處理、加載狀態和請求取消等。這些最佳實踐不僅能提升應用的可維護性和可讀性,還能極大地改善使用者體驗。本篇文章將介紹如何在前端優雅地處理 API 請求
你可以參考之前的兩篇文章,了解更多有關 AJAX 和常見 HTTP 的請求方式:
如何優雅接 API?
選擇 Axios 替代 XHR 和 Fetch
選擇 Axios
取代 XHR
和 fetch
,因為 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 最佳實踐
如果後端是用 header
帶 token
來驗證登入狀態,可以使用攔截器,讓全部的 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
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>