本文 AI 產出,尚未審核

Vue3 教學:非同步與資料請求 ── 多重資料請求組合


簡介

在單頁應用 (SPA) 中,畫面往往需要同時顯示多筆來自不同 API 的資料,例如使用者資訊、商品清單、即時通知等。若每一次請求都必須等前一筆完成才能發起,將嚴重拖慢使用者體驗。因此,掌握「多重資料請求」的組合技巧,是 Vue3 開發者在打造高效能、流暢介面時的關鍵能力。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步了解如何在 Vue3 中同時發送多筆非同步請求、合併結果,並安全地在組件生命週期內管理這些請求。即使你是剛踏入 Vue3 的新手,只要跟著範例走,也能快速上手。


核心概念

1. Promise 與 Promise.allPromise.allSettled

Vue3 本身不提供 HTTP 客戶端,通常會搭配 Axiosfetchvue-query。這些函式都會回傳 Promise,而 Promise 是 JavaScript 處理非同步工作的基石。

  • Promise.all(iterable):所有 Promise 必須全部成功才會 resolve,若任一失敗則立即 reject。
  • Promise.allSettled(iterable):不管成功或失敗,都會在全部完成後返回每一筆結果的狀態,適合「即使有錯誤也要顯示其他資料」的情境。
// 範例:同時發送兩筆請求,全部成功才繼續
Promise.all([axios.get('/api/user'), axios.get('/api/orders')])
  .then(([userRes, ordersRes]) => {
    // 兩筆資料都已取得
  })
  .catch(error => {
    // 只要有一筆失敗,就會進到這裡
  });

2. async/awaittry/catch 的結合

async/await 讓非同步程式碼看起來像同步流程,配合 try/catch 可以更直觀地捕捉錯誤。

async function fetchData() {
  try {
    const [user, orders] = await Promise.all([
      axios.get('/api/user'),
      axios.get('/api/orders')
    ]);
    // 成功取得資料
  } catch (e) {
    // 任意一筆失敗都會跑到這裡
  }
}

3. Vue3 setuprefreactive 的搭配

在 Composition API 中,我們通常使用 refreactive 來保存非同步取得的資料,並在 setup 中觸發請求。

import { ref, onMounted } from 'vue';
import axios from 'axios';

export default {
  setup() {
    const user = ref(null);
    const orders = ref([]);
    const loading = ref(true);
    const error = ref(null);

    async function loadData() {
      try {
        const [uRes, oRes] = await Promise.all([
          axios.get('/api/user'),
          axios.get('/api/orders')
        ]);
        user.value = uRes.data;
        orders.value = oRes.data;
      } catch (e) {
        error.value = e;
      } finally {
        loading.value = false;
      }
    }

    onMounted(loadData);

    return { user, orders, loading, error };
  }
};

4. 取消請求 (AbortController)

在組件被銷毀前,如果仍有未完成的請求,繼續等待會產生記憶體洩漏或不必要的錯誤訊息。AbortController 能在 onUnmounted 時取消尚未完成的 fetch。

import { onMounted, onUnmounted, ref } from 'vue';

export default {
  setup() {
    const controller = new AbortController();
    const data = ref(null);
    const error = ref(null);

    async function fetch() {
      try {
        const res = await fetch('/api/data', { signal: controller.signal });
        data.value = await res.json();
      } catch (e) {
        if (e.name !== 'AbortError') error.value = e;
      }
    }

    onMounted(fetch);
    onUnmounted(() => controller.abort());

    return { data, error };
  }
};

小技巧:Axios 也支援取消請求,只要在建立實例時傳入 signal 即可。

5. 使用 vue-query(或 @tanstack/vue-query)管理多筆請求

vue-query 為 React Query 在 Vue 生態的實作,提供 快取、重試、背景更新 等功能,讓多筆請求的組合變得更簡潔。

import { useQuery } from '@tanstack/vue-query';
import axios from 'axios';

function useDashboardData() {
  const userQuery = useQuery(['user'], () => axios.get('/api/user').then(r => r.data));
  const ordersQuery = useQuery(['orders'], () => axios.get('/api/orders').then(r => r.data));

  // 同步兩筆結果
  const combined = computed(() => ({
    user: userQuery.data.value,
    orders: ordersQuery.data.value,
    isLoading: userQuery.isLoading.value || ordersQuery.isLoading.value,
    isError: userQuery.isError.value || ordersQuery.isError.value,
  }));

  return combined;
}

程式碼範例

以下提供 5 個實用範例,涵蓋最常見的多重資料請求情境,並附上說明。

範例 1️⃣:簡易的 Promise.all 結合

情境:同時取得使用者資訊與最近的 5 筆訂單。

import { ref, onMounted } from 'vue';
import axios from 'axios';

export default {
  setup() {
    const user = ref(null);
    const recentOrders = ref([]);
    const loading = ref(true);
    const error = ref(null);

    async function load() {
      try {
        const [uRes, oRes] = await Promise.all([
          axios.get('/api/user'),
          axios.get('/api/orders?limit=5')
        ]);
        user.value = uRes.data;
        recentOrders.value = oRes.data;
      } catch (e) {
        error.value = e;
      } finally {
        loading.value = false;
      }
    }

    onMounted(load);
    return { user, recentOrders, loading, error };
  }
};

重點:只要其中一筆失敗,catch 會捕捉到錯誤,且 loading 會在 finally 中關閉。


範例 2️⃣:Promise.allSettled 讓失敗不阻斷成功結果

情境:同時載入「熱門商品」與「最新消息」,即使「最新消息」API 暫時掛掉,也要顯示「熱門商品」。

import { ref, onMounted } from 'vue';
import axios from 'axios';

export default {
  setup() {
    const hotProducts = ref([]);
    const news = ref([]);
    const loading = ref(true);
    const errors = ref([]);

    async function load() {
      const results = await Promise.allSettled([
        axios.get('/api/products/hot'),
        axios.get('/api/news/latest')
      ]);

      results.forEach((result, idx) => {
        if (result.status === 'fulfilled') {
          if (idx === 0) hotProducts.value = result.value.data;
          else news.value = result.value.data;
        } else {
          errors.value.push(result.reason);
        }
      });

      loading.value = false;
    }

    onMounted(load);
    return { hotProducts, news, loading, errors };
  }
};

技巧allSettled 讓我們能分別處理每筆請求的成功或失敗,而不會因單筆失敗而中斷整體流程。


範例 3️⃣:使用 AbortController 取消未完成的請求

情境:使用者在搜尋框快速切換關鍵字,舊的搜尋請求需要被取消,避免回傳過時資料。

import { ref, watch, onUnmounted } from 'vue';

export default {
  setup() {
    const query = ref('');
    const results = ref([]);
    const loading = ref(false);
    const error = ref(null);
    let controller = null; // 變數放在外層,讓 watch 能存取

    watch(query, async (newQ) => {
      if (!newQ) {
        results.value = [];
        return;
      }

      // 取消前一次請求
      if (controller) controller.abort();

      controller = new AbortController();
      loading.value = true;
      error.value = null;

      try {
        const res = await fetch(`/api/search?q=${encodeURIComponent(newQ)}`, {
          signal: controller.signal
        });
        const data = await res.json();
        results.value = data;
      } catch (e) {
        if (e.name !== 'AbortError') error.value = e;
      } finally {
        loading.value = false;
      }
    });

    onUnmounted(() => {
      if (controller) controller.abort();
    });

    return { query, results, loading, error };
  }
};

重點:每次 query 改變,都會先取消上一次的請求,避免競爭條件 (race condition)。


範例 4️⃣:useQuery (vue-query) 同步多筆資料

情境:Dashboard 需要同時顯示「使用者資訊」與「待處理任務」,且希望自動快取與背景重新抓取。

import { useQuery } from '@tanstack/vue-query';
import axios from 'axios';
import { computed } from 'vue';

export default {
  setup() {
    const userQuery = useQuery(['user'], () =>
      axios.get('/api/user').then(res => res.data)
    );

    const tasksQuery = useQuery(['tasks', 'pending'], () =>
      axios.get('/api/tasks?status=pending').then(res => res.data)
    );

    const dashboard = computed(() => ({
      user: userQuery.data.value,
      pendingTasks: tasksQuery.data.value,
      isLoading: userQuery.isLoading.value || tasksQuery.isLoading.value,
      isError: userQuery.isError.value || tasksQuery.isError.value,
    }));

    return { dashboard };
  }
};

優點vue-query 會自動處理快取、錯誤重試、背景重新抓取 (stale-while-revalidate),大幅減少手寫的狀態管理程式碼。


範例 5️⃣:串接 依賴性請求(先取得 ID 再抓細節)

情境:先取得商品列表,使用者點選某商品後,同時發送兩筆請求:商品詳細與相關評論。

import { ref, watchEffect } from 'vue';
import axios from 'axios';

export default {
  setup() {
    const selectedProductId = ref(null);
    const productDetail = ref(null);
    const productReviews = ref([]);
    const loading = ref(false);
    const error = ref(null);

    watchEffect(async () => {
      if (!selectedProductId.value) return;

      loading.value = true;
      error.value = null;

      try {
        const [detailRes, reviewsRes] = await Promise.all([
          axios.get(`/api/products/${selectedProductId.value}`),
          axios.get(`/api/products/${selectedProductId.value}/reviews`)
        ]);
        productDetail.value = detailRes.data;
        productReviews.value = reviewsRes.data;
      } catch (e) {
        error.value = e;
      } finally {
        loading.value = false;
      }
    });

    return {
      selectedProductId,
      productDetail,
      productReviews,
      loading,
      error
    };
  }
};

說明watchEffect 會在 selectedProductId 改變時自動重新執行,保證畫面永遠顯示最新的商品資訊與評論。


常見陷阱與最佳實踐

陷阱 為什麼會發生 解決方案 / 最佳實踐
1. 競爭條件 (Race Condition) 多次快速觸發請求,較晚回傳的結果覆蓋較早的資料 使用 AbortController 取消舊請求,或在回傳前檢查「最新的請求 token」
2. 錯誤被吞掉 Promise.all 中任一失敗會直接走到 catch,失去其他成功結果 若需要保留成功結果,改用 Promise.allSettled,或自行包裝每筆 Promise 為 try/catch
3. 記憶體洩漏 組件卸載後仍持續等待請求完成,會觸發 setState 警告 onUnmounted 中呼叫 abort(),或使用 vue-query 自動清理
4. 重複請求 多個組件或多次 setup 內呼叫相同 API,浪費頻寬 使用快取機制 (axios interceptor + cache, vue-query);或將請求抽成共用 composable
5. 不一致的 loading 狀態 同時管理多筆 loading,容易出現「只顯示一筆」的情況 將所有 loading 合併為 `isLoading = a.isLoading

最佳實踐摘要

  1. 先設計資料流:確定哪些請求是必須同步完成,哪些可以獨立或容錯。
  2. 使用 Promise.allSettled:在 UI 必須顯示部分成功結果時,避免因單筆失敗導致全局失效。
  3. 加入取消機制:尤其在搜尋、分頁、路由切換等頻繁觸發的情境。
  4. 快取與重試:使用 axios-cache-adaptervue-query 或自行實作快取層,減少重複請求。
  5. 統一錯誤處理:建立全局的錯誤攔截器 (axios interceptor) 或集中式的 error 狀態,讓 UI 能統一呈現。

實際應用場景

場景 為什麼需要多重請求 建議的實作方式
Dashboard 首頁 必須同時呈現「使用者資訊」+「今日統計」+「即時通知」 使用 Promise.allSettled + vue-query 快取,保持 UI 即時更新
商品詳細頁 商品資訊、庫存、相關評論、相似商品四筆資料 先取得商品 ID → Promise.all 抓取其餘三筆;若評論失敗仍顯示其他資訊
搜尋結果 同時顯示「搜尋結果」與「搜尋建議」 AbortController 取消舊請求 + Promise.allSettled 讓建議失敗不影響結果
表單編輯 需要先抓取「表單結構」與「預設值」兩筆資料 Promise.all 確保兩者皆成功再渲染表單;失敗時顯示錯誤提示
多語系切換 切換語系時,同時載入「翻譯檔」與「本地化圖檔」 使用 useQueries (vue-query) 同時發起,利用快取避免重複下載

總結

多重資料請求是 Vue3 應用中提升效能與使用者體驗的核心技巧。透過 Promise 系列方法 (allallSettled)、async/awaitAbortController,以及 vue-query 這類先進的資料抓取套件,我們可以:

  • 同時發送多筆請求,縮短等待時間
  • 容錯處理,即使部分 API 失效也能顯示可用資料
  • 安全取消,避免記憶體洩漏與競爭條件
  • 快取與自動重試,減少不必要的流量與提升穩定性

掌握上述概念與範例後,你就能在任何需要同時顯示多筆資料的 Vue3 專案中,寫出 乾淨、可維護且具彈性的程式碼。祝開發順利,期待在你的下一個 Vue3 專案裡看到這些技巧的實際運用! 🚀