Vue3 教學:非同步與資料請求 ── 多重資料請求組合
簡介
在單頁應用 (SPA) 中,畫面往往需要同時顯示多筆來自不同 API 的資料,例如使用者資訊、商品清單、即時通知等。若每一次請求都必須等前一筆完成才能發起,將嚴重拖慢使用者體驗。因此,掌握「多重資料請求」的組合技巧,是 Vue3 開發者在打造高效能、流暢介面時的關鍵能力。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步了解如何在 Vue3 中同時發送多筆非同步請求、合併結果,並安全地在組件生命週期內管理這些請求。即使你是剛踏入 Vue3 的新手,只要跟著範例走,也能快速上手。
核心概念
1. Promise 與 Promise.all、Promise.allSettled
Vue3 本身不提供 HTTP 客戶端,通常會搭配 Axios、fetch 或 vue-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/await 與 try/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 setup 與 ref、reactive 的搭配
在 Composition API 中,我們通常使用 ref 或 reactive 來保存非同步取得的資料,並在 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 |
最佳實踐摘要
- 先設計資料流:確定哪些請求是必須同步完成,哪些可以獨立或容錯。
- 使用
Promise.allSettled:在 UI 必須顯示部分成功結果時,避免因單筆失敗導致全局失效。 - 加入取消機制:尤其在搜尋、分頁、路由切換等頻繁觸發的情境。
- 快取與重試:使用
axios-cache-adapter、vue-query或自行實作快取層,減少重複請求。 - 統一錯誤處理:建立全局的錯誤攔截器 (axios interceptor) 或集中式的
error狀態,讓 UI 能統一呈現。
實際應用場景
| 場景 | 為什麼需要多重請求 | 建議的實作方式 |
|---|---|---|
| Dashboard 首頁 | 必須同時呈現「使用者資訊」+「今日統計」+「即時通知」 | 使用 Promise.allSettled + vue-query 快取,保持 UI 即時更新 |
| 商品詳細頁 | 商品資訊、庫存、相關評論、相似商品四筆資料 | 先取得商品 ID → Promise.all 抓取其餘三筆;若評論失敗仍顯示其他資訊 |
| 搜尋結果 | 同時顯示「搜尋結果」與「搜尋建議」 | AbortController 取消舊請求 + Promise.allSettled 讓建議失敗不影響結果 |
| 表單編輯 | 需要先抓取「表單結構」與「預設值」兩筆資料 | Promise.all 確保兩者皆成功再渲染表單;失敗時顯示錯誤提示 |
| 多語系切換 | 切換語系時,同時載入「翻譯檔」與「本地化圖檔」 | 使用 useQueries (vue-query) 同時發起,利用快取避免重複下載 |
總結
多重資料請求是 Vue3 應用中提升效能與使用者體驗的核心技巧。透過 Promise 系列方法 (all、allSettled)、async/await、AbortController,以及 vue-query 這類先進的資料抓取套件,我們可以:
- 同時發送多筆請求,縮短等待時間
- 容錯處理,即使部分 API 失效也能顯示可用資料
- 安全取消,避免記憶體洩漏與競爭條件
- 快取與自動重試,減少不必要的流量與提升穩定性
掌握上述概念與範例後,你就能在任何需要同時顯示多筆資料的 Vue3 專案中,寫出 乾淨、可維護且具彈性的程式碼。祝開發順利,期待在你的下一個 Vue3 專案裡看到這些技巧的實際運用! 🚀