Axios 模組化實戰:從封裝進化到專業的檔案架構
在 前一章 我們學會了如何封裝 Axios,但將所有邏輯塞在同一個檔案並不利於 團隊協作 與 單元測試。模組化 (Modularization) 的核心價值在於 職責分離 ,讓開發者能更快速地定位問題並擴充功能。
為什麼需要模組化?
模組化不僅是為了好看,更是為了實現以下目標:
- 單一職責原則 (SRP):每個檔案只負責一件事(建立實體、處理邏輯或攔截請求)。
- 提高複用性:元件不再依賴特定的 Axios 配置,而是直接呼叫工具模組。
- 目錄結構化:讓新進成員能一眼看出專案的 API 邏輯置於何處。
推薦的專案目錄結構
在現代前端開發中,我們通常將與框架無關、可複用的工具函數置於 utils 目錄。Axios 的模組化建議採用以下路徑:
src/
├─ utils/ # 通用工具函式庫
│ └─ axios/ # Axios 模組化核心
│ ├─ instance.js # 負責環境設定與實體建立
│ ├─ api.js # 負責通用方法封裝 (GET/POST...)
│ └─ interceptors.js # 負責 Token 與錯誤處理攔截
├─ App.vue
└─ main.js # 進入點,負責註冊攔截器模組化檔案詳解
實體設定 (instance.js)
專注於 在哪裡請求 以及 請求的預設規則 。
import axios from 'axios';
const instance = axios.create({
baseURL:
import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json',
},
});
export default instance;baseURL 交給 環境變數 決定,測試機與正式機就不必改任何程式碼。
通用方法封裝 (api.js)
將 Axios 原生的方法轉化為更符合專案需求、更乾淨的呼叫介面。
import instance from './instance';
export default {
async GET(endPoint, config = {}) {
try {
const res = await instance.get(endPoint, config);
return res.data;
} catch (err) {
console.log(err);
}
},
};錯誤請往外丟,不要只印出來
上面的 console.log(err) 只適合開發時觀察。這樣寫呼叫端會拿到 undefined,卻不知道請求失敗,畫面也就無法顯示錯誤訊息。實務上建議統一改成:
} catch (err) {
return Promise.reject(err);
}要顯示什麼、要不要重試,交給呼叫端的 try...catch 決定。
邏輯攔截器 (interceptors.js)
將複雜的認證邏輯與錯誤狀態碼處理抽離成獨立函式。
import instance from './instance';
export default () => {
instance.interceptors.request.use(
(req) => {
const token = localStorage.getItem('token');
if (token) req.headers.Authorization = `Bearer ${token}`;
return req;
},
(err) => Promise.reject(err)
);
instance.interceptors.response.use(
(res) => res,
(err) => {
switch (err?.response?.status) {
case 401:
console.log('權限過期');
break;
case 403:
console.log('沒有權限');
break;
case 500:
console.log('伺服器錯誤');
break;
default:
console.log('網路有問題');
break;
}
return Promise.reject(err);
}
);
};在 main.js 全域註冊
import { createApp } from 'vue';
import App from './App.vue';
// 匯入攔截器初始化函式
import setupInterceptors from '@/utils/axios/interceptors';
// 執行註冊
setupInterceptors();
createApp(App).mount('#app');為什麼要在這裡註冊?
建立應用程式 的進入點只會執行一次,能保證攔截器 註冊一次且早於任何請求。如果寫在元件裡,元件每次掛載都會再註冊一組,同一個錯誤就會跳出好幾次提示。
再進一步:依業務拆分 API 模組
當請求數量變多,api.js 只提供 GET / POST 這種 通用方法 已經不夠,建議在上面再疊一層 業務層:
src/
├─ utils/axios/ # 通用層:實體、方法、攔截器
└─ api/ # 業務層:依功能分類
├─ user.js # 登入、取得個人資料
├─ product.js # 商品列表、商品明細
└─ order.js # 訂單建立與查詢// api/user.js
import api from '@/utils/axios/api';
export const login = (payload) => api.POST('/auth/login', payload);
export const getProfile = () => api.GET('/user/profile');元件端就只剩業務語意,完全看不到網址與 Token:
<script setup>
import { ref, onMounted } from "vue";
import { getProfile } from "@/api/user";
const profile = ref(null);
onMounted(async () => {
try {
profile.value = await getProfile();
} catch (err) {
console.log("取得個人資料失敗");
}
});
</script>若這些請求邏輯需要在多個元件共用,還可以再包成 Composable;共用的資料狀態則適合交給 Pinia 管理。
延伸:utils 目錄的其他必備工具
除了 Axios,一個成熟的專案通常會在 utils 目錄下放置以下功能,讓開發更有效率:
- 格式化:日期、貨幣、數字格式化工具。
- Storage:封裝
localStorage與sessionStorage的讀取與過期處理。 - 驗證工具:表單欄位、Email、手機號碼的正則表達式驗證。
- 效率相關:常用的
debounce或throttle函式。 - 權限判斷。
- 常數管理。
透過這種 高內聚、低耦合 的模組化管理,您的專案將具備更高的健壯性,也能輕鬆應對日益成長的業務需求!
常見問題
攔截器為什麼要在 main.js 註冊?
為了確保整個應用程式啟動時就完成註冊,而且只註冊一次。若寫在元件裡,元件每次掛載都會重複註冊,同一個錯誤會出現多次提示。
api.js 裡的錯誤該印出來還是往外丟?
建議往外丟。只用 console.log 會讓呼叫端拿到 undefined 卻不知道失敗,畫面無法顯示錯誤狀態。統一 return Promise.reject(err),由呼叫端決定要怎麼呈現。
API 檔案要依業務拆分嗎?
專案有多個功能模組時建議拆。把 user、product、order 各自成檔,函式名稱直接對應業務語意,元件只需匯入需要的那一個檔案。
為什麼放在 utils 而不是 api 目錄?
兩種都常見。utils 強調它是與框架無關的通用工具,api 則強調業務語意。實務上可以把 instance 與 interceptors 放 utils,業務請求函式另外放 api 目錄。
模組化之後元件要怎麼呼叫?
元件只匯入業務層的函式並直接 await,不需要知道 baseURL、Token 或錯誤碼處理。這樣元件內只剩畫面邏輯,測試時也容易替換掉請求層。