本文 AI 產出,尚未審核

Vue3 – 非同步與資料請求:async setup()


簡介

在 Vue 3 中,Composition API 讓我們可以把組件的邏輯拆解成更易於復用的 function。而 setup() 正是這個新世界的入口。隨著前端應用越來越依賴遠端 API、即時資料流或複雜的非同步運算,setup() 中直接使用 async/await 成為了最直觀、最符合語意的寫法。

async setup() 的好處不只讓程式碼看起來更乾淨,它還能:

  1. 在組件掛載前完成資料取得,避免畫面閃爍或顯示空白狀態。
  2. 與 Vue 的響應式系統無縫結合,取得的資料自動變為 ref,後續變更會自動觸發重新渲染。
  3. 簡化錯誤處理與 loading 狀態管理,讓開發者可以把關注點集中在業務邏輯上。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整帶你掌握 async setup() 在 Vue3 專案中的實務應用。


核心概念

1. setup() 基礎回顧

位置 功能
setup(props, context) 組件建立時第一個被呼叫的函式。此時尚未產生 this,只能透過參數取得 propsemitslots 等資訊。
回傳值 只要回傳 Object,其中的屬性會被自動注入到模板 (template) 中。常見的回傳類型有 refreactivecomputed、以及自訂的函式。

注意setup() 內部不允許使用 this,所有需要的資料必須透過 refreactivecomputed 來管理。

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() 中直接回傳 refreactive,Vue 仍會保持它們的響應式特性,無需額外包裝。

4. 與 onMountedwatchEffect 的關係

方法 何時執行 常見用途
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() 讓我們在模板渲染前就取得使用者資料,loadingerror 兩個狀態也同步管理。


範例 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️⃣ 結合 Suspenseasync 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>

說明:當 UserProfileasync setup() 還在等待 API 回傳時,父層的 <Suspense> 會自動顯示 fallback 區塊,提供更流暢的使用者體驗。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方案
setup() 中直接修改 props Vue 會發出警告,且資料不具備響應式 使用 toRefs(props) 或在 watch 中同步
忘記返回 ref/reactive 模板無法取得資料,顯示 undefined 確認 return { xxx } 包含所有需要在模板使用的變數
async setup() 中拋出未捕獲的錯誤 組件掛載失敗,整個應用可能卡住 使用 try/catch 包裹 await,或在全域 errorHandler 處理
過度使用 await,導致串行請求 效能下降(尤其是多個不相關的 API) 使用 Promise.allPromise.allSettledaxios.all
setup() 中使用 this thisundefined,導致程式崩潰 只使用 refcomputedwatch,切勿依賴 this
把大量資料計算放在 setup() 初始渲染時間過長 把計算搬到 computedwatchEffect,或利用懶載入 (lazy)

最佳實踐清單

  1. 保持 setup() 簡潔:只做「取得資料 + 建立響應式狀態」的事,其他邏輯移到 composable(自訂 Hook)中。
  2. 使用 ref 包裝非同步結果:即使是物件或陣列,也建議使用 ref,避免因直接回傳非 ref 而失去響應式。
  3. 統一錯誤與 loading 管理:可以建立一個 useFetch composable,回傳 { data, loading, error },在各組件中直接使用。
  4. 善用 Suspense:對於必須在渲染前完成的資料,配合 <Suspense> 能提供更好的使用者體驗。
  5. 避免在 setup() 中直接呼叫 router.push:若需要根據資料判斷導向,建議在 onMountedwatch 中處理,避免在還未掛載時就改變路由。

實際應用場景

場景 為何適合使用 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 中處理非同步資料最直觀、最具表達力的方式。
  • 透過 awaitPromise.allaxios 攔截器、以及 Suspense,我們可以在組件渲染前完成資料取得、錯誤處理與 loading 管理。
  • 為了避免常見的陷阱,建議將非同步邏輯封裝成 composable,保持 setup() 的簡潔與可讀性。
  • 在實務開發中,從使用者資訊、商品詳情到大型儀表板,async setup() 都能提供更好的效能與使用者體驗,尤其在 SSR 與 SEO 需求日益重要的今天,掌握這項技巧將大幅提升前端開發的品質與效率。

最後:若你已熟悉傳統的 Options API,建議先在小型專案中練習 async setup(),逐步將其引入到既有的 Vue 3 專案。只要遵循「保持 setup() 簡潔」的原則,搭配適當的錯誤與 loading 管理,你會發現開發非同步功能變得前所未有的順手。祝你在 Vue3 的旅程中玩得愉快! 🚀