本文 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,只能透過參數取得 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)

最佳實踐清單

  1. 保持 setup() 簡潔:只做「取得資料 + 建立響應式狀態」的事,其他邏輯移到 composable(自訂 Hook)中。
  2. 使用 ref 包裝非同步結果:即使是物件或陣列,也建議使用 ref,避免因直接回傳非 ref 而失去響應式。
  3. 統一錯誤與 loading 管理:可以建立一個 useFetch composable,回傳 { data, loading, error },在各組件中直接使用。
  4. 善用 Suspense:對於必須在渲染前完成的資料,配合 <Suspense> 能提供更好的使用者體驗。
  5. 避免在 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 的旅程中玩得愉快! 🚀