---
# System prepended metadata

title: "\U0001F4B8 網站流量費好貴！學校老師沒告訴你的 AbortController 省錢術 ✨！"
tags: [Web Development, TanStack Query, AbortController, 前端效能優化, 前端開發, 網站優化, 前端流量優化, VueJS]

---

---
title: '💸 網站流量費好貴！學校老師沒告訴你的 AbortController 省錢術 ✨！'
disqus: hackmd
description: Vue + TanStack Query 實戰教學，教你用 AbortController 節省頻寬、減少無效請求，避免爆流量費！
keywords: [TanStack Query, AbortController, Vue3, Nuxt4, Axios signal, cancelQueries, 前端效能優化, 高併發, 節流, 流量費, 架構師教學]
---
# 💸 網站流量費好貴！學校老師沒告訴你的 AbortController 省錢術 ✨！

![截圖 2025-11-12 下午6.32.54](https://hackmd.io/_uploads/ByfDzJGeWx.png)


## 都用了 TanStack Query，為何還需要 AbortController？🤔

> 這篇源自我上週在 Vue 群開的投票，壓倒性票數選了這個主題。
> 然後跟架構師朋友竹子（Willy）聊了聊，發現這個誤會比我想像的還深...
> 所以來寫篇文章好好講清楚 😎

本文適合使用 Vue + TanStack Query 開發大型後台或高頻操作頁面的工程師。

---

## 先說結論 🎯

| | TanStack Query | AbortController |
|---|---|---|
| **解決什麼問題** | 相同 key 的請求去重、Cache 管理、背景重新驗證 | 真正終止網路請求、節省頻寬、避免競態條件 |
| **無法解決** | 不同 key 的 in-flight requests | Cache 管理 |
| **適合場景** | 靜態資料、低頻更新 | 即時性業務、頻繁切換路由/Tab |

**核心概念**：
* TanStack Query 只是「停止觀察」，不是「終止請求」
* AbortController 是「取消請求」，但只有純前端部分，實質上還是要依賴後端監聽http status，實務上後端若用 Node.js、Go 或 NestJS，可透過 req.aborted 事件偵測前端中斷請求，立即停止查詢，真正節省伺服器成本。

---

## 起因：竹子的靈魂拷問 💬

前幾個禮拜竹子跟我聊到：

> **竹子**：
「tanstack query 因為是 **cache** 管理，觸發請求的時機不是在 onMounted，而是 **setup 就發出請求了**。
我的觀點是 **request abort 是因為組件被卸載時，但請求還沒結束**，這時 abort 可以節省資源。但**如果用 tanstack query 因為是完成後直接存到 cache，反覆請求資源也都會節流，應用 cache 的資料，所以沒有需要 abort**。資料生命週期跟組件生命週期脫鉤，不但能節省流量，也可以背景處理。

> 蠻想知道當使用 tanstack query 時，你認為 abort 使用的時機點在什麼時候？我覺得可能有我沒想到的情境，想問問你的想法。」

我當下的反應：「咦？這個誤會有點大...」

然後我們就開始了一場技術討論（聊了快一小時 XD）

---

## 問題在哪？Cache ≠ 請求終止 🚫

換句話說，TanStack Query 管的是「資料狀態」，AbortController 管的是「網路生命週期」

先來個簡單的例子：

```typescript
// 情境：你有 5 個頁面
const pages = [
  { path: '/wallet/deposit', key: 'deposit' },
  { path: '/wallet/withdraw', key: 'withdraw' },
  { path: '/trade/futures/orders', key: 'futures' },
  { path: '/trade/perpetual/positions', key: 'perpetual' },
  { path: '/users/list', key: 'users' }
];
```

### TanStack Query 的行為

```typescript
// 每個頁面都是獨立的 useQuery
useQuery({ queryKey: ['deposit'], queryFn: fetchDeposit });
useQuery({ queryKey: ['withdraw'], queryFn: fetchWithdraw });
useQuery({ queryKey: ['futures'], queryFn: fetchFutures });
```

**重點來了**：
- ✅ 如果你在同一個頁面重複打 `['deposit']`，會被去重
- ❌ 但如果你快速切換 5 個頁面，5 個請求**全部會發出**（因為 key 不同，會被視為應該執行）

### 真實痛點

OP 人員可能同時開 10+ 個分頁在操作。點擊不同路由的連結有時候會很快

使用者行為：瘋狂點側邊欄切換頁面或者瀏覽器的上一頁（奶油手，肯定是常態）

**後果**：
```
👉 5 個頁面 × 3 個 API = 15 個請求同時發出
👉 其中數個 Response 可能總體積變得有點大
👉 使用者已經離開頁面了，請求還在背景跑 （除非是下載文件檔案允許背景進行）
👉 頻寬爆炸 💥 流量費帳單哭哭 😭
```

---

## 真實場景：我遇到的三個大坑 🕳️

### 場景 1：Tab 狂點（年/月/日報表）

```vue
<el-tabs v-model="activeTab">
  <el-tab-pane label="日報表" name="daily" />
  <el-tab-pane label="月報表" name="monthly" />
  <el-tab-pane label="年報表" name="yearly" />
</el-tabs>
```

**業務需求**：
- OP 人員切換 Tab 要**馬上看到資料**（不想按搜尋）
- 報表資料隨時在變，**不能用舊 cache**

**TanStack Query 的問題**：
```typescript
// 使用者手抖，1 秒內狂點 3 次
useQuery({
  queryKey: ['report', activeTab.value],
  queryFn: () => fetchReport(activeTab.value),
  staleTime: 0  // 關掉 cache，確保即時性
});

// ❌ 結果：daily、monthly、yearly 三個請求同時發出
```

**實際影響**：
- 單個報表 API Response：**1.5MB**
- 3 個請求，但真正用到的只有最後一個 = 流量浪費

---

### 場景 2：即時性業務（訂單、流水）

> **狀況**：「這些流水和帳變之類的報表數字隨時在變，都不能因為 cache 問題少更新。」

```typescript
// ❌ 不適合用 TanStack Query 的場景
useQuery({
  queryKey: ['orders'],
  staleTime: 30000,  // 30 秒內用 cache
  // 問題：訂單可能 1 秒內就新增/更新了
});
```

**為什麼不用**：
1. **業務要求即時性** → 每次進頁面都要最新資料，Cache 反而阻礙了正常行爲
2. **失去 TanStack Query 優勢** → 既然不用 cache，幹嘛引入這層複雜度？

---

### 場景 3：多頁面快速切換

```typescript
// 使用者操作：首頁 → 錢包 → 出金 → 使用者列表
// 每個頁面停留時間：< 1 秒

// ❌ 沒有 Abort 的結果
// 4 個頁面的 API 請求全部發出
// 其中 2 個 Response 超過 2MB
// 總流量變高 💸

// ✅ 有 Abort 的結果
// 只有最後停留頁面的個請求
// 總流量不會變得太大 💰
```

**改善幅度**：
- 請求數減少：**至少30%** ⬇️
- 流量減少：**約50%，但還是要依賴後端監聽http狀態是否取消** ⬇️
- 效能：**少掉不必要的後續動作，當然會變得更好** ⚡

---

## 解法：AbortController 出場 🦸

### 核心工具：`abortManager.ts`

這是我專案裡最關鍵的 util，用 **Map + Set** 管理所有請求：

```typescript
/** 追蹤所有請求的 controller */
const keyMap = new Map<string, Set<AbortController>>();
const controllers = new Set<AbortController>();

/** 追蹤 controller（可選帶 key） */
export function track(c: AbortController, key?: string) {
  controllers.add(c);
  if (!key) return;
  if (!keyMap.has(key)) keyMap.set(key, new Set());
  keyMap.get(key)!.add(c);
}

/** 移除追蹤 */
export function untrack(c: AbortController) {
  controllers.delete(c);
  keyMap.forEach(set => set.delete(c));
}

/** 終止所有請求 */
export function abortAll() {
  controllers.forEach(c => c.abort());
  controllers.clear();
}

/** 只取消指定 key 的請求 */
export function abortKey(key: string) {
  const set = keyMap.get(key);
  if (!set) return;
  set.forEach(c => c.abort());
  set.clear();
}
```

**設計亮點**：
1. **Set 不重複** → 同一個 controller 不會被追蹤多次
2. **Key-based 管理** → 可以針對
---
特定 API 終止（不是無腦全殺）
3. **全域清理** → 路由切換時一鍵清空

### Axios 整合：自動注入 Signal

```typescript
import { track } from '@/utils/abortManager';

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 120000
});

// 請求攔截器：自動塞 signal
request.interceptors.request.use((config) => {
  const controller = new AbortController();
  config.signal = controller.signal;

  // 追蹤 controller（用 URL path 當 key）
  const key = config.url?.split('?')[0]; // 去掉 query string
  track(controller, key);

  return config;
});

// 響應攔截器：處理 Abort 錯誤
request.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.name === 'CanceledError')  {
      console.log('Request cancelled:', error.message);
      // 不要顯示錯誤訊息給使用者（這是正常行為）
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);
```

---

### Composable 層：路由 Hooks 整合

```typescript
// src/views/finance/composables/useDepositRecords.ts
import { onBeforeRouteLeave } from 'vue-router';
import { abortAll, abortKey } from '@/utils/abortManager';
import to from 'await-to-js';

export function useReportDaily() {
  const RequestKey = {
    REPORT_DAILY: 'report/daily',
    REPORT_MONTHLY: 'report/monthly'
  } as const;

  const getReportDaily = async () => {
    // 1. 先終止相同 key 的舊請求（防止重複提交）
    abortKey(RequestKey.REPORT_DAILY);

    // 2. 發出新請求
    const [err, res] = await to(fetchReportDaily(form.value));
    // ...
  };

  // ✅ 離開頁面時終止所有請求
  onBeforeRouteLeave(() => {
    abortAll();
  });

  return { getReportDaily };
}
```

**關鍵點**：
1. **離開頁面前清理** → `onBeforeRouteLeave(() => abortAll())`
2. **防止重複提交** → 提交前先 `abortKey()` 終止舊請求
3. **精準控制** → 針對不同 API 使用不同 key

---

### 白名單機制：報表下載不能砍

```typescript
// 有些請求不該被 abort（例如：檔案下載）
const ABORT_WHITELIST = [
  '/api/export/csv',
  '/api/export/excel',
  '/api/report/download'
];

export function track(c: AbortController, key?: string) {
  if (key && ABORT_WHITELIST.includes(key)) {
    // 白名單內的請求不追蹤，讓它背景慢慢跑
    return;
  }
  controllers.add(c);
  // ...
}
```

**適用場景**：
- 📊 報表下載（使用者期待背景完成）
- 📁 檔案上傳（中斷會導致資料遺失）
- 💳 重要交易（必須完成的寫入操作）

---

## 跟竹子的技術討論（精華節錄）💡

### 🤔 問：TanStack Query 的 debounce 不夠用嗎？

> **竹子**：「TanStack Query 本身就有 debounce，應該能避免重複請求吧？」

**我的回答**：

Debounce 只能**延遲發出**，不能**終止已發出的請求**。

```typescript
// TanStack Query 的 debounce
useQuery({
  queryKey: ['search', keyword.value],
  queryFn: () => searchAPI(keyword.value),
  // ⚠️ debounce 只能「延後發出」，不能「砍掉已發出的」
});

// 使用者快速輸入：'a' → 'ab' → 'abc'
// 結果：3 個請求都發出了（只是有延遲）
```

**AbortController 的優勢**：
```typescript
// 使用者輸入時，立刻砍掉舊搜尋
watch(keyword, () => {
  abortKey('search');  // 先砍
  search(keyword.value);  // 再打
});
```

---

### 🤔 問：為什麼不統一用一種方案？

> **竹子**：「有些人討厭不統一的做法，但事實上就是依照需求特性調整做法」

**我的回答**：

技術選型要基於**業務場景**，不統一是怕太發散，就是我常講的沒有正常的制度會導致很大的災難。

| 場景 | 適合方案 | 原因 |
|------|---------|------|
| 下拉選單資料 | TanStack Query | 靜態資料，適合 cache |
| 即時訂單列表 | AbortController | 需要最新資料，cache 沒意義 |
| 報表 Tab 切換 | AbortController | 頻繁切換，避免流量浪費 |
| 使用者資料查詢 | TanStack Query + Abort | 結合兩者優勢 |

**實務建議**：
```typescript
// 1. 靜態資料：純 TanStack Query
const { data } = useQuery({
  queryKey: ['currencies'],
  queryFn: fetchCurrencies,
  staleTime: Infinity  // 永久 cache，爽
});

// 2. 即時資料：純 AbortController
const getOrders = async () => {
  abortKey('orders');
  const [err, res] = await to(fetchOrders());
  // ...
};

// 3. 混合場景：兩者都要
const { data, refetch } = useQuery({
  queryKey: ['userDetail', userId.value],
  queryFn: ({ signal }) => fetchUser(userId.value, { signal }),
  staleTime: 30000
});

onBeforeRouteLeave(() => {
  queryClient.cancelQueries(['userDetail']);
});
```

---

## 常見誤區（我踩過的坑）🕳️

### ❌ 誤區 1：以為 TanStack Query 會自動砍請求

```typescript
// ❌ 錯誤認知
const { data } = useQuery({
  queryKey: ['orders'],
  queryFn: fetchOrders
});
// 組件卸載時，請求「不會」被終止
// 只是結果不會更新到組件而已
```

**正確做法**：
```typescript
// ✅ 手動傳 signal（或在 Axios 全域處理）
const { data } = useQuery({
  queryKey: ['orders'],
  queryFn: ({ signal }) => fetchOrders({ signal })
});
```

---

### ❌ 誤區 2：濫用 abortAll()

```typescript
// ❌ 不好：提交表單時全部砍掉
const handleSubmit = () => {
  abortAll();  // 可能誤殺其他重要請求
  submitForm();
};
```

**正確做法**：
```typescript
// ✅ 用 abortKey() 精準打擊
const handleSubmit = () => {
  abortKey('form/submit');  // 只砍同類請求
  submitForm();
};
```

---

### ❌ 誤區 3：忘記清理 controller

```typescript
// ❌ 忘記 untrack，controller 會一直留在 Set 裡
const controller = new AbortController();
track(controller, 'myAPI');
// 請求完成後，controller 還在...
```

**正確做法**：
```typescript
// ✅ 在 Axios interceptor 統一處理
request.interceptors.response.use(
  (response) => {
    const controller = getControllerFromSignal(response.config.signal);
    if (controller) untrack(controller);
    return response;
  },
  (error) => {
    const controller = getControllerFromSignal(error.config?.signal);
    if (controller) untrack(controller);
    return Promise.reject(error);
  }
);
```

---

## Nuxt 開發者看這邊！👀

### 問：Nuxt 也需要手動處理 AbortController 嗎？

早些時間點當我把這篇文章分享到 Vue 群時就有人問：

> **「Nuxt 是不是也要自己實作 AbortController？」**

**好消息：Nuxt 4.2+ 已經內建支援了！** 🎉

---

### Nuxt 4.2 的自動化方案

從 2025 年 10 月發佈的 Nuxt 4.2 開始，框架已經內建 AbortController 支援：

[Nuxt blog: 4.2 Released ](https://nuxt.com/blog/v4-2#abort-control-for-data-fetching)

#### 1️⃣ **useAsyncData 自動接收 signal**

```typescript
// ✅ Nuxt 4.2+ 的做法
const { data, error, clear, refresh } = await useAsyncData(
  'users',
  (_nuxtApp, { signal }) => $fetch('/api/users', { signal })
)

// 離開頁面時，Nuxt 會自動 abort
```

**關鍵差異**：
- Vue 3：需要手動在 `onBeforeRouteLeave` 呼叫 `abortAll()`
- Nuxt 4.2：框架自動處理，handler 接收內建的 `signal`

---

#### 2️⃣ **dedupe 選項控制請求策略**

```typescript
const { data, refresh } = await useAsyncData(
  'report',
  async (_nuxtApp, { signal }) => {
    return $fetch('/api/report', { signal })
  },
  {
    dedupe: 'cancel'  // ⬅️ 關鍵設定
  }
)

// 當你快速切換 Tab 時：
onMounted(() => {
  refresh()  // 會自動 abort 前一個請求
})
```

**dedupe 兩種模式**：
| 模式 | 行為 | 適用場景 |
|------|------|----------|
| `cancel` | 新請求會 abort 舊請求 | Tab 切換、搜尋防抖 |
| `defer` | 等舊請求完成才發新的 | 避免 API rate limit |

---

#### 3️⃣ **手動控制：傳入自己的 AbortController**

```typescript
const abortController = new AbortController()

const { refresh } = await useAsyncData(
  'longTask',
  (_nuxtApp, { signal }) => $fetch('/api/heavy', { signal })
)

// 使用者點擊「取消」按鈕
const handleCancel = () => {
  abortController.abort()
  refresh({ signal: abortController.signal })
}
```

---

### 實際效果比較

**情境**：使用者快速切換「日報表 → 月報表 → 年報表」

注意，體積是舉例，實際上還是要看你的總Request有多少

| 方案 | 請求數 | 頻寬消耗 |
|------|--------|----------|
| 原始 Vue 3（無 Abort） | 3 個 | 4.5MB |
| Vue 3 + 手動 AbortController | 1 個 | 1.5MB ✅ |
| Nuxt 4.2（dedupe: cancel） | 1 個 | 1.5MB ✅ |

**結論**：Nuxt 4.2 開箱即用，不需要自己寫 `abortManager.ts`！

---

### 什麼時候還是需要手動處理？

即使用了 Nuxt 4.2，以下場景還是建議自己管理：

#### ❌ 場景 1：非 Composable 的請求

```typescript
// ❌ 直接用 axios，Nuxt 管不到
import axios from 'axios'

const handleSubmit = async () => {
  // 這個請求不會被 Nuxt 自動 abort
  const res = await axios.post('/api/submit', form.value)
}
```

**解法**：改用 `$fetch` 或手動加 AbortController。

---

#### ❌ 場景 2：多個獨立請求同時發出

```typescript
// ❌ 5 個請求有不同 key，Nuxt 無法去重
const fetchAllData = async () => {
  await Promise.all([
    useAsyncData('deposit', () => $fetch('/api/deposit')),
    useAsyncData('withdraw', () => $fetch('/api/withdraw')),
    useAsyncData('futures', () => $fetch('/api/futures')),
    useAsyncData('perpetual', () => $fetch('/api/perpetual')),
    useAsyncData('users', () => $fetch('/api/users'))
  ])
}
```

**解法**：在 `onBeforeRouteLeave` 統一清理，或用本文的 `abortManager.ts`。

---

#### ❌ 場景 3：需要白名單機制

```typescript
// Nuxt 的 dedupe 是全域行為，無法針對特定 API 關閉
// 例如：報表下載不想被 abort

const { data } = await useAsyncData(
  'download',
  () => $fetch('/api/export/excel'),
  { dedupe: 'cancel' }  // ⚠️ 還是會被 abort
)
```

**解法**：使用本文的白名單機制，手動控制哪些請求不該被砍。

---

### Nuxt vs Vue 3

| 面向 | Vue 3 | Nuxt 4.2+ |
|------|-------|-----------|
| **AbortController 支援** | 需自己實作 | 內建於 `useAsyncData` |
| **離開頁面自動清理** | 需手動 `onBeforeRouteLeave` | 框架自動處理 |
| **dedupe 去重** | 需自己寫邏輯 | 內建 `cancel` / `defer` 模式 |
| **白名單機制** | 可自訂 | 無內建，需自己處理 |
| **適用範圍** | 所有請求方式 | 僅限 `useFetch` / `useAsyncData` |

**建議**：
- **純 Nuxt 專案**：優先用框架內建功能（`dedupe: 'cancel'`）
- **混合場景**（有 axios / 自訂請求）：還是需要本文的 `abortManager.ts`
- **需要白名單**：手動實作（Nuxt 沒提供）

---

## 如果我單純只想用TanStack Query呢?

當然可以!! 我們這篇文章講的就是Abort的重要性和Request KEY的區別
稍早前竹子的LINE群也有熱心的朋朋幫忙補充:

![image](https://hackmd.io/_uploads/S1j715Mx-e.png)

>Danny:
**cancelQueries** 會觸發 **retryer.cancel()**，而 **retryer** 會呼叫 **abortController.abort()**。
但只有當你的 queryFn 把這個 signal 傳給 fetch/axios 時，請求才會被真正中止。


我們先從**TanStack Query v5（query-core）** 原始碼來看:

### queryClient.cancelQueries()

cancelQueries() 會找到所有符合條件的 Query，
逐一呼叫每個 query.cancel()。

```typescript=
cancelQueries<TTaggedQueryKey extends QueryKey = QueryKey>(
  filters?: QueryFilters<TTaggedQueryKey>,
  cancelOptions: CancelOptions = {},
): Promise<void> {
  const defaultedCancelOptions = { revert: true, ...cancelOptions }
  const promises = notifyManager.batch(() =>
    this.#queryCache
      .findAll(filters)
      .map((query) => query.cancel(defaultedCancelOptions)),
  )
  return Promise.all(promises).then(noop).catch(noop)
}

```

### Query.cancel()

它呼叫了內部的 this.#retryer.cancel()，這才是實際中止請求的地方。

```typescript=
cancel(options?: CancelOptions): Promise<void> {
  const promise = this.#retryer?.promise
  this.#retryer?.cancel(options)
  return promise ? promise.then(noop).catch(noop) : Promise.resolve()
}

```

### createRetryer() 裡的 onCancel

當 queryClient.cancelQueries() 觸發 → 進入這個 onCancel。
它會呼叫 abortController.abort()。

若你的 fetchFn（也就是 queryFn）內有吃進這個 signal，
則會真正中止網路請求。

```typescript=
this.#retryer = createRetryer({
  fn: context.fetchFn as () => Promise<TData>,
  onCancel: (error) => {
    if (error instanceof CancelledError && error.revert) {
      this.setState({ fetchStatus: 'idle' as const })
    }
    abortController.abort()
  },
  ...
})
```

### abortController 的建立

這一段會自動注入 signal 到傳給 fetchFn 的 context 物件中。
只要你的 queryFn 宣告 ({ signal }) => ... 就能取得這個 signal。

```typescript=
const abortController = new AbortController()
const addSignalProperty = (object: unknown) => {
  Object.defineProperty(object, 'signal', {
    enumerable: true,
    get: () => abortController.signal,
  })
}
```

### 來實踐看看

```typescript=
const { data } = useQuery({
  queryKey: ['orders'],
  queryFn: ({ signal }) => axios.get('/api/orders', { signal }),
})

```
:::info
💡 當呼叫 cancelQueries() 時：
* 會透過 retryer.cancel() 觸發 onCancel()。
* 內部呼叫 abortController.abort()。

若 queryFn 有使用 ({ signal }) => fetch(..., { signal })，
就會真的中止網路請求。

若沒接 signal：Query 仍會停止觀察結果，但請求會照樣跑完。
:::


---

## 總結：隨「業務需求」去適應 🎨

這篇講的是流量省錢，但本質其實是對「前端資源生命周期的掌控力」。
當你能主動終止無意義的浪費，就代表你對架構設計才真正開始啟蒙。

### TanStack Query 的優勢 ✨

- ✅ 相同 key 的請求去重
- ✅ 自動背景重新驗證
- ✅ Optimistic updates
- ✅ 分頁 / 無限滾動支援
- ✅ DevTools 可視化

**適合場景**：
- 靜態資料（幣種、國家列表）
- 使用者資料（個人檔案）
- 低頻更新的資料

---

### AbortController 的優勢 ⚡

- ✅ 真正終止網路請求
- ✅ 節省頻寬與伺服器資源
- ✅ 避免競態條件
- ✅ 精準控制請求生命週期
- ✅ 適應各種業務場景

**適合場景**：
- 即時性業務（訂單、流水）
- 頻繁切換（Tab、路由）
- 大型 Response（報表）
- 搜尋防抖 + 立即終止舊請求

---

### 最終建議 💡

**不要教條式地選技術，而是看業務需求靈活組合。**

當然你不需要跟我一樣手刻AbortController，以 TanStack 的現有Solution絕對可以獲得更方便的支援哦!!

```typescript
// 🎯 理想架構：分層處理
// 1. Axios 層：全域 AbortController 管理
// 2. Service 層：提供統一的 API 介面
// 3. Composable 層：根據業務選 TanStack Query 或 Abort
// 4. Component 層：專注於 UI 邏輯
```

---

## 延伸閱讀 📚

- [MDN - AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
- [TanStack Query - Cancellation](https://tanstack.com/query/latest/docs/react/guides/query-cancellation)
- [Axios - Cancellation](https://axios-http.com/docs/cancellation)
- [Vue Router - Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)

---

**特別感謝** 🙏
感謝竹子（Willy）跟我聊了一個小時，讓這篇文章的論述更完整。
- 竹子的脆：[@f2e_willy](https://www.threads.com/@f2e_willy)
- 我的脆：[@astolfo_proto](https://www.threads.com/@astolfo_proto)

---

如果這篇文章對你有幫助，歡迎按讚分享！👇

###### tags: `VueJS`, `AbortController`, `TanStack Query`, `前端開發` `Web Development`, `前端效能優化`, `前端流量優化`, `網站優化`