Vue3 – 非同步與資料請求:async setup()
簡介
在 Vue 3 中,Composition API 讓我們可以把組件的邏輯拆解成更易於復用的 function。而 setup() 正是這個新世界的入口。隨著前端應用越來越依賴遠端 API、即時資料流或複雜的非同步運算,在 setup() 中直接使用 async/await 成為了最直觀、最符合語意的寫法。
async setup() 的好處不只讓程式碼看起來更乾淨,它還能:
- 在組件掛載前完成資料取得,避免畫面閃爍或顯示空白狀態。
- 與 Vue 的響應式系統無縫結合,取得的資料自動變為
ref,後續變更會自動觸發重新渲染。 - 簡化錯誤處理與 loading 狀態管理,讓開發者可以把關注點集中在業務邏輯上。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你掌握 async setup() 在 Vue3 專案中的實務應用。
核心概念
1. setup() 基礎回顧
| 位置 | 功能 |
|---|---|
setup(props, context) |
組件建立時第一個被呼叫的函式。此時尚未產生 this,只能透過參數取得 props、emit、slots 等資訊。 |
| 回傳值 | 只要回傳 Object,其中的屬性會被自動注入到模板 (template) 中。常見的回傳類型有 ref、reactive、computed、以及自訂的函式。 |
注意:
setup()內部不允許使用this,所有需要的資料必須透過ref、reactive或computed來管理。
2. 何時需要把 setup() 變成 async
setup() 本身是一個同步函式,若在裡面直接呼叫非同步 API,會得到一個 Promise,但 Vue 不會等它 resolve 後才繼續渲染。為了在 組件渲染前 完成資料載入,我們可以:
export default {
async setup() {
// 這裡可以直接使用 await
}
}
此時 Vue 會 暫停掛載流程,等 setup() resolve 後才繼續執行後續的生命週期(如 onMounted)。
3. async setup() 的返回值
當 setup() 被標記為 async,它會回傳一個 Promise。Vue 會等待這個 Promise resolve,然後把 resolve 後的 Object 注入模板。例如:
export default {
async setup() {
const data = await fetchData()
return { data } // data 會自動變成 ref
}
}
提示:若
setup()中直接回傳ref、reactive,Vue 仍會保持它們的響應式特性,無需額外包裝。
4. 與 onMounted、watchEffect 的關係
| 方法 | 何時執行 | 常見用途 |
|---|---|---|
async setup() |
組件建立階段、渲染前 | 需要在畫面出現前完成的資料請求、權限驗證 |
onMounted |
真正掛載到 DOM 後 | 操作 DOM、觸發動畫、或是一次性的非同步任務 |
watchEffect |
任意時刻,只要依賴變更 | 監聽響應式資料變化,動態發送請求 |
實務建議:如果資料不需要在首次渲染前就完整取得,將請求搬到
onMounted會更符合「先渲染、後補資料」的使用者體驗。
程式碼範例
以下提供 五個實用範例,分別示範不同情境下的 async setup() 用法。
範例 1️⃣ 基本的非同步資料取得
<script setup>
import { ref } from 'vue'
// 假設有一個簡易的 API
async function fetchUser(id) {
const resp = await fetch(`https://api.example.com/users/${id}`)
return resp.json()
}
// async setup() 直接取得資料
const userId = 1
const user = ref(null) // 先宣告 ref
const loading = ref(true)
const error = ref(null)
try {
const data = await fetchUser(userId) // await 直接寫在 setup 中
user.value = data
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
</script>
<template>
<div v-if="loading">載入中…</div>
<div v-else-if="error">錯誤:{{ error }}</div>
<div v-else>
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
</div>
</template>
說明:
async setup()讓我們在模板渲染前就取得使用者資料,loading、error兩個狀態也同步管理。
範例 2️⃣ 同時發送多個請求(Promise.all)
<script setup>
import { ref } from 'vue'
async function fetchPosts() {
const resp = await fetch('https://api.example.com/posts')
return resp.json()
}
async function fetchComments() {
const resp = await fetch('https://api.example.com/comments')
return resp.json()
}
// 同時取得文章與評論
const posts = ref([])
const comments = ref([])
const loading = ref(true)
try {
const [postData, commentData] = await Promise.all([
fetchPosts(),
fetchComments()
])
posts.value = postData
comments.value = commentData
} finally {
loading.value = false
}
</script>
<template>
<div v-if="loading">資料載入中…</div>
<div v-else>
<h3>文章列表 ({{ posts.length }})</h3>
<ul>
<li v-for="p in posts" :key="p.id">{{ p.title }}</li>
</ul>
<h3>評論列表 ({{ comments.length }})</h3>
<ul>
<li v-for="c in comments" :key="c.id">{{ c.body }}</li>
</ul>
</div>
</template>
重點:使用
Promise.all可以在 同一時間 發送多個請求,縮短等待時間。async setup()的寫法讓整段流程保持線性、易讀。
範例 3️⃣ 搭配 vue-router 取得路由參數
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
async function fetchProduct(id) {
const resp = await fetch(`https://api.example.com/products/${id}`)
return resp.json()
}
const route = useRoute()
const product = ref(null)
const loading = ref(true)
const productId = Number(route.params.id) // 取得路由參數
try {
product.value = await fetchProduct(productId)
} finally {
loading.value = false
}
</script>
<template>
<div v-if="loading">載入商品資訊…</div>
<div v-else-if="!product">找不到商品</div>
<div v-else>
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<p>價格:{{ product.price }} 元</p>
</div>
</template>
說明:在
async setup()中直接使用useRoute(),可以在組件渲染前即取得路由參數,避免在onMounted之後才去抓資料造成閃爍。
範例 4️⃣ 使用 axios 並加上攔截器(全域錯誤處理)
<script setup>
import { ref } from 'vue'
import axios from 'axios'
// 建立一個 axios 實例,加入攔截器
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 8000
})
// 全域錯誤攔截
api.interceptors.response.use(
response => response,
err => {
console.error('API 錯誤:', err)
// 這裡可以做統一的錯誤提示
return Promise.reject(err)
}
)
async function fetchOrders() {
const { data } = await api.get('/orders')
return data
}
const orders = ref([])
const loading = ref(true)
const error = ref(null)
try {
orders.value = await fetchOrders()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
</script>
<template>
<div v-if="loading">載入訂單…</div>
<div v-else-if="error">發生錯誤:{{ error }}</div>
<div v-else>
<h3>最近 10 筆訂單</h3>
<ul>
<li v-for="o in orders" :key="o.id">
訂單 #{{ o.id }} - 金額 {{ o.amount }} 元
</li>
</ul>
</div>
</template>
技巧:將
axios實例與攔截器抽離成獨立檔案,可在多個組件共用,async setup()只負責呼叫 API,保持單一職責。
範例 5️⃣ 結合 Suspense 與 async setup()(進階)
Vue 3 的 <Suspense> 可以讓我們在子組件的 async setup() 尚未完成時,顯示備援 UI。
<!-- Parent.vue -->
<template>
<Suspense>
<template #default>
<UserProfile :userId="42" />
</template>
<template #fallback>
<p>載入使用者資訊中…</p>
</template>
</Suspense>
</template>
<!-- UserProfile.vue -->
<script setup>
import { ref } from 'vue'
async function getUser(id) {
const r = await fetch(`https://api.example.com/users/${id}`)
return r.json()
}
const props = defineProps({
userId: { type: Number, required: true }
})
const user = ref(null)
user.value = await getUser(props.userId) // async setup
</script>
<template>
<div v-if="user">
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
</div>
</template>
說明:當
UserProfile的async setup()還在等待 API 回傳時,父層的<Suspense>會自動顯示fallback區塊,提供更流暢的使用者體驗。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案 |
|---|---|---|
在 setup() 中直接修改 props |
Vue 會發出警告,且資料不具備響應式 | 使用 toRefs(props) 或在 watch 中同步 |
忘記返回 ref/reactive |
模板無法取得資料,顯示 undefined |
確認 return { xxx } 包含所有需要在模板使用的變數 |
在 async setup() 中拋出未捕獲的錯誤 |
組件掛載失敗,整個應用可能卡住 | 使用 try/catch 包裹 await,或在全域 errorHandler 處理 |
過度使用 await,導致串行請求 |
效能下降(尤其是多個不相關的 API) | 使用 Promise.all、Promise.allSettled 或 axios.all |
在 setup() 中使用 this |
this 為 undefined,導致程式崩潰 |
只使用 ref、computed、watch,切勿依賴 this |
把大量資料計算放在 setup() |
初始渲染時間過長 | 把計算搬到 computed 或 watchEffect,或利用懶載入 (lazy) |
最佳實踐清單
- 保持
setup()簡潔:只做「取得資料 + 建立響應式狀態」的事,其他邏輯移到 composable(自訂 Hook)中。 - 使用
ref包裝非同步結果:即使是物件或陣列,也建議使用ref,避免因直接回傳非ref而失去響應式。 - 統一錯誤與 loading 管理:可以建立一個
useFetchcomposable,回傳{ data, loading, error },在各組件中直接使用。 - 善用
Suspense:對於必須在渲染前完成的資料,配合<Suspense>能提供更好的使用者體驗。 - 避免在
setup()中直接呼叫router.push:若需要根據資料判斷導向,建議在onMounted或watch中處理,避免在還未掛載時就改變路由。
實際應用場景
| 場景 | 為何適合使用 async setup() |
|---|---|
| 使用者資訊頁面(需要在首次渲染前取得使用者資料) | 可避免 UI 閃爍,直接在 setup() 完成資料請求。 |
| 商品詳情(依據路由參數取得商品) | 路由參數在 setup() 已可取得,配合 async 直接呼叫 API。 |
| 儀表板(Dashboard)(同時載入多個統計圖表) | 使用 Promise.all 同時抓多筆資料,減少等待時間。 |
| 需要全域錯誤統一處理的企業系統 | 在 async setup() 中統一使用封裝好的 useApi,讓錯誤與 loading 變成可重用的狀態。 |
| SSR(伺服器端渲染) | async setup() 讓伺服器在回傳 HTML 前完成資料取得,保證首屏即有完整內容。 |
案例:在一個電商平台的商品詳情頁,開發者將
async setup()與Suspense結合,先顯示「載入中」的骨架畫面,等到商品資料與評論都取得後一次性渲染。這樣不僅提升 SEO(因為 SSR 已經把資料注入),也讓使用者感受到更流暢的體驗。
總結
async setup()是 Vue 3 中處理非同步資料最直觀、最具表達力的方式。- 透過 await、Promise.all、axios 攔截器、以及 Suspense,我們可以在組件渲染前完成資料取得、錯誤處理與 loading 管理。
- 為了避免常見的陷阱,建議將非同步邏輯封裝成 composable,保持
setup()的簡潔與可讀性。 - 在實務開發中,從使用者資訊、商品詳情到大型儀表板,
async setup()都能提供更好的效能與使用者體驗,尤其在 SSR 與 SEO 需求日益重要的今天,掌握這項技巧將大幅提升前端開發的品質與效率。
最後:若你已熟悉傳統的 Options API,建議先在小型專案中練習
async setup(),逐步將其引入到既有的 Vue 3 專案。只要遵循「保持setup()簡潔」的原則,搭配適當的錯誤與 loading 管理,你會發現開發非同步功能變得前所未有的順手。祝你在 Vue3 的旅程中玩得愉快! 🚀