Skip to content

Axios 封裝實戰:打造高效能且易維護的 API 請求層

Axios 封裝:axios.create 與攔截器

在開發中大型專案時,如果每次發送 API 請求都要重複設定 timeoutheaders 或手動處理 401 錯誤,不僅程式碼冗餘,管理起來更是噩夢。為了提高 程式碼維護性擴充性,對 Axios 進行二次封裝是前端開發者的必修課。

若還不熟悉基本語法,建議先看 Axios 使用

為什麼需要封裝 Axios?

傳統的寫法會將邏輯與配置混在一起,如下所示:

js
axios({
  method: 'get',
  url: 'https://jsonplaceholder.typicode.com/users',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'xxx',
  },
})
  .then((res) => {
    console.log(res.data);
  })
  .catch((err) => {
    const status = err?.response?.status;

    if (status === 401) console.log('權限過期');
    if (status === 403) console.log('無此權限');

    console.log(err);
  });

這段程式碼的問題不是不能跑,而是 每支 API 都要複製一次。網址換了要改幾十個地方,新增一個錯誤碼提示也要逐檔補上。

小提醒

透過封裝,我們可以將 統一配置錯誤處理環境變數 抽離,實現更優雅的程式碼結構。

如何為 Axios 進行封裝

我們必須統一設定,例如:請求標頭、狀態碼、超過請求的時間、不同環境設置不同接口,並將請求的方法再次封裝與設置請求與回應的攔截器。

Axios 實體化 (Create Instance)

利用 axios.create 建立實體,可以針對不同的 API 來源(例如:測試環境 vs 正式環境)設置不同的基礎路徑。

  • baseURL:建議搭配環境變數,例如:process.env.VITE_API_URL 來切換環境。
  • timeout:統一設置請求超時時間,避免無限等待。
  • headers:設置預設的請求標頭。如果有特殊標頭以參數的方式傳入,將會覆蓋預設的請求標頭。
js
const instance = axios.create({
  baseURL:
    import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
  },
});

為什麼不直接改 axios.defaults?

axios.defaults全域設定,一旦專案要同時打自家 API 與第三方服務,兩邊的 baseURL 與 Token 就會互相干擾。axios.create 產生的是獨立實體,各自帶著自己的設定與攔截器,互不影響。環境變數的設定方式可參考 環境變數

Axios 方法封裝 (API Methods)

將常用的 GETPOST 等方法再次封裝,自訂回傳格式,例如:直接回傳 res.data,能讓呼叫端的程式碼更乾淨。

js
const api = {
  async GET(endPoint, config = {}) {
    try {
      const res = await instance.get(endPoint, config);
      return res.data;
    } catch (err) {
      return Promise.reject(err);
    }
  },
  async POST(endPoint, data, config = {}) {
    try {
      const res = await instance.post(endPoint, data, config);
      return res.data;
    } catch (err) {
      return Promise.reject(err);
    }
  },
};

封裝後呼叫端只剩一行,而且拿到的直接就是資料本體:

js
const users = await api.GET('/users', { params: { _limit: 10 } });

Axios 攔截器 (Interceptors) 的核心應用

攔截器是封裝的靈魂,它能在 請求送出前回應收到後 進行加工。

請求攔截 (Request Interceptor)

主要用於 自動注入 Token。不需要在每次發送 API 時手動傳入 Authorization。

回應攔截 (Response Interceptor)

主要用於 統一錯誤處理,例如:401 自動登出、500 系統錯誤提示。

js
function interceptors() {
  instance.interceptors.request.use(
    (req) => req,
    (err) => {}
  );
  instance.interceptors.response.use(
    (res) => res,
    (err) => {}
  );
}

interceptors();

再做一點補強,如下:

js
function interceptors() {
  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);
    }
  );
}

interceptors();

參考來源:Axios

攔截器的兩個必寫細節

  • 一定要 return:請求攔截器要回傳 req,回應攔截器要回傳 res,忘記寫請求就會停在這裡不再往下走。
  • 錯誤要往外丟:處理完共同邏輯後必須 return Promise.reject(err),否則呼叫端的 catch 不會觸發,畫面會誤以為請求成功、loading 也關不掉。

401 之後該做什麼?

只印出 權限過期 沒有實際幫助,通常會接著清掉憑證並導回登入頁。搭配 Vue Router 時可以這樣寫:

js
import router from '@/router';

instance.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err?.response?.status === 401) {
      localStorage.removeItem('token');
      router.push({ name: 'login' });
    }

    return Promise.reject(err);
  }
);

若專案有 導航守衛,兩者會形成完整的防線:守衛負責 進頁面前 檢查,攔截器負責 API 回應後 補救。

Token 放哪裡?

localStorage 方便但會被同源的 JavaScript 讀取,遇到 XSS 時可能外洩。安全需求較高的專案會改用 HttpOnly Cookie,或縮短 Token 有效期並搭配更新機制。

Axios 封裝的分層職責

透過這樣的結構,當 API URL 變更或需要新增全域錯誤提示時,您只需要修改一個地方,就能影響整個專案,這就是封裝的核心價值!

模組檔案主要職責
instance.js負責 axios.create 基礎設定與環境變數對應。
api.js封裝成更易讀的 GET / POST 方法,簡化回傳資料格式。
interceptors.js集中處理請求前的 Token 注入與回應後的狀態碼判斷。

上面的三個檔案怎麼實際拆開、放在哪個目錄,請看 Axios 模組化

常見問題

為什麼要用 axios.create 而不是直接改 axios.defaults?

axios.defaults 會影響全域,專案同時要打多個網域或有第三方 API 時容易互相干擾。axios.create 產生獨立實體,每組來源可以有自己的 baseURLtimeout 與攔截器。

攔截器裡一定要 return 嗎?

一定要。請求攔截器必須回傳 config,回應攔截器必須回傳 response,否則請求會停在攔截器不再往下走。錯誤處理則要回傳 Promise.reject(err),才不會把錯誤吞掉。

為什麼錯誤處理完還要 Promise.reject?

因為攔截器只負責共同處理,例如:顯示提示或登出。若不往外丟,呼叫端的 catch 不會被觸發,畫面會以為請求成功,載入狀態也關不掉。

baseURL 要怎麼切換測試與正式環境?

把網址寫進環境變數,例如:Vite 的 import.meta.env.VITE_API_URL,再由 build 指令決定載入哪一份 env 檔,程式碼就不需要為了換環境而修改。

Token 存在 localStorage 安全嗎?

localStorage 會被同源的 JavaScript 讀取,遭遇 XSS 時 Token 可能外洩。安全性要求較高的專案通常改用 HttpOnly Cookie,或縮短 Token 有效期並搭配更新機制。

封裝與模組化差在哪裡?

封裝是把重複的設定與錯誤處理集中起來,模組化 則是進一步把實體、方法與攔截器拆成獨立檔案。先封裝解決重複,再模組化解決檔案過長與職責混雜。

延伸閱讀