本文 AI 產出,尚未審核
Vue3 非同步與資料請求
主題:非同步元件(defineAsyncComponent)
簡介
在單頁應用(SPA)中,隨著功能日益龐大,首屏載入的體驗往往會因為一次性載入過多的程式碼而受影響。Vue 3 為了解決這個問題,提供了 非同步元件(Async Component) 的機制,讓開發者可以把較不常使用或體積較大的元件,等到真正需要時才動態載入。
defineAsyncComponent 是 Vue 3 官方推薦的 API,它不僅支援基本的懶載入,還提供了 載入中、錯誤、超時 等回饋機制,讓使用者在等待過程中不會看到空白頁面,同時也能更好地處理網路錯誤。掌握這項技術,能顯著降低首屏 bundle 大小、提升應用效能,並且改善使用者體驗,是現代前端開發不可或缺的功力。
核心概念
1. 為什麼要使用非同步元件?
- 減少首屏載入時間:只載入當前路由或當前視圖真正需要的元件,其他元件保留在伺服器端,待使用時再下載。
- 提升效能:瀏覽器同時下載的資源有限,將較大的元件拆分成多個小檔,可讓瀏覽器更有效率地利用網路帶寬。
- 改善使用者體驗:配合 loading、error UI,使用者不會因為等待而感到卡頓或不確定。
2. defineAsyncComponent 基本語法
Vue 3 內建的 defineAsyncComponent 接受一個返回 Promise 的函式,這個 Promise 必須 resolve 為一個元件定義(Component)。最簡單的寫法如下:
import { defineAsyncComponent } from 'vue'
const AsyncHello = defineAsyncComponent(() => import('./components/Hello.vue'))
在模板中使用時,和普通元件沒有差別:
<template>
<AsyncHello />
</template>
3. 進階選項:loading、error、delay、timeout
defineAsyncComponent 也接受一個 物件,可以設定以下屬性:
| 屬性 | 型別 | 說明 |
|---|---|---|
loader |
() => Promise<Component> |
必填,返回元件的函式 |
loadingComponent |
Component |
載入期間顯示的元件 |
errorComponent |
Component |
載入失敗時顯示的元件 |
delay |
number (ms) |
在顯示 loadingComponent 前的延遲時間,預設 200ms |
timeout |
number (ms) |
載入逾時的時間,逾時會觸發 errorComponent,預設無限制 |
onError |
(error, retry, fail, attempts) => void |
自訂錯誤處理,允許手動 retry 或 fail |
範例 1:最完整的非同步元件寫法
import { defineAsyncComponent } from 'vue'
// 1. 先建立 Loading、Error 兩個小元件
const LoadingSpinner = {
template: `<div class="spinner">載入中…</div>`
}
const LoadError = {
template: `<div class="error">載入失敗,請稍後再試。</div>`
}
// 2. 使用 defineAsyncComponent
export const AsyncChart = defineAsyncComponent({
// 必填:載入目標元件
loader: () => import('./components/Chart.vue'),
// 載入期間顯示的 UI
loadingComponent: LoadingSpinner,
// 載入失敗時的 UI
errorComponent: LoadError,
// 只在超過 300ms 後才顯示 LoadingSpinner,避免閃爍
delay: 300,
// 10 秒仍未成功載入即視為逾時
timeout: 10000,
// 自訂錯誤處理:允許使用者重試
onError(error, retry, fail, attempts) {
if (attempts < 3) {
// 最多重試三次
console.warn(`載入失敗,第 ${attempts + 1} 次重試…`)
retry()
} else {
fail()
}
}
})
Tip:
loader必須回傳 ES Module(即import()),因此不要使用require或 CommonJS。
4. 與 Vue Router 的結合
在路由層級直接使用非同步元件,可以讓整個路由分支在第一次切換時才下載相對應的程式碼。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { defineAsyncComponent } from 'vue'
const UserProfile = defineAsyncComponent(() => import('../views/UserProfile.vue'))
const Settings = defineAsyncComponent(() => import('../views/Settings.vue'))
const routes = [
{
path: '/user/:id',
component: UserProfile
},
{
path: '/settings',
component: Settings
}
]
export default createRouter({
history: createWebHistory(),
routes
})
5. 動態載入多個元件(組件庫)
如果你有一個大型的 UI 套件(例如 ElementPlus、Ant Design Vue),可以把每個子元件做成非同步的,減少一次性載入的重量。
// utils/asyncUI.js
import { defineAsyncComponent } from 'vue'
export const AsyncButton = defineAsyncComponent(() =>
import('element-plus/lib/components/button')
)
export const AsyncDialog = defineAsyncComponent(() =>
import('element-plus/lib/components/dialog')
)
在需要的地方直接引用:
<template>
<AsyncButton type="primary" @click="open">開啟對話框</AsyncButton>
<AsyncDialog v-model="showDialog" title="非同步對話框">
<p>這是一個使用 async component 的 Dialog。</p>
</AsyncDialog>
</template>
<script setup>
import { ref } from 'vue'
import { AsyncButton, AsyncDialog } from '@/utils/asyncUI'
const showDialog = ref(false)
const open = () => (showDialog.value = true)
</script>
常見陷阱與最佳實踐
| 陷阱 | 原因 | 解決方式 |
|---|---|---|
未設定 delay,導致短暫的 loading UI 閃爍 |
loadingComponent 立即顯示,會在 10~30ms 內出現閃爍 |
設定 delay: 200(或根據實際情況調整) |
忘記 timeout,在網路不佳時永遠卡住 loading |
loader 永遠不 resolve,使用者無法得知失敗原因 |
加上 timeout: 8000(8 秒)或自訂 onError |
| loader 回傳非 ES Module,導致錯誤 | import() 必須回傳 default 匯出的元件 |
確認 export default 正確,或使用 () => import('./Comp.vue').then(m => m.default) |
| 過度切分,導致過多 HTTP 請求 | 每個小元件都獨立請求,會產生過多連線 | 使用 webpackChunkName 或 vite 的 manualChunks 合理分組 |
| 在 SSR 中使用,因為 SSR 需要同步的元件 | 非同步載入在伺服器端無法即時取得 | 在 SSR 環境下使用 defineAsyncComponent 時,確保 loader 能在伺服器端同步執行或使用 ssr: false 的方式跳過 |
最佳實踐
- 適度切分:只針對「首次載入不必要」或「體積較大」的元件使用 async。
- 提供 Loading UI:即使是 200ms 延遲,也建議提供簡潔的 spinner,提升感知效能。
- 錯誤重試機制:使用
onError讓使用者有機會手動重試,尤其在行動裝置或不穩定的網路環境。 - 命名分塊:在 Vite 中,可使用
/* webpackChunkName: "user-profile" */註解來自訂 chunk 名稱,方便除錯與 CDN 緩存。 - 配合 Prefetch/Preload:對於即將進入的路由,可在
<router-link>上加上prefetch,提前下載下一個 async component。
<router-link to="/settings" v-slot="{ navigate, href }">
<a :href="href" @click="navigate" rel="prefetch">設定</a>
</router-link>
實際應用場景
| 場景 | 為何適合使用 defineAsyncComponent |
|---|---|
| 大型儀表板:包含多個圖表、地圖、報表 | 圖表套件(如 echarts)體積龐大,僅在使用者切換到相關分頁時才載入 |
| 電商商品詳情頁:每個商品可能需要不同的評論、推薦模型 | 評論區塊、即時推薦列表可分別非同步載入,提升主圖與基本資訊的渲染速度 |
| 管理後台的設定頁:功能眾多但使用頻率低 | 如「權限管理」或「系統日誌」等頁面,可延遲載入,減少首頁載入時間 |
| 行動端 APP:網路環境多變 | 透過 timeout + retry 機制,確保在慢速網路下仍能提供回饋 |
| 多語系或主題切換:不同語系/主題可能需要不同的 UI 元件 | 只在使用者切換語系或主題時才載入對應的字體或樣式組件 |
範例:電商商品頁的非同步評論區
// components/ProductReview.vue
import { defineAsyncComponent } from 'vue'
export const AsyncReview = defineAsyncComponent({
loader: () => import('./ReviewList.vue'),
loadingComponent: {
template: `<div class="review-loading">載入評論中…</div>`
},
errorComponent: {
template: `<div class="review-error">評論載入失敗,<button @click="$emit('retry')">重試</button></div>`
},
delay: 200,
timeout: 8000,
onError(error, retry, fail) {
// 若是 404(評論功能尚未開啟),直接顯示空狀態
if (error.message.includes('404')) fail()
else retry()
}
})
<template>
<section class="product-detail">
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<!-- 只在需要時載入評論 -->
<AsyncReview @retry="reloadReview" />
</section>
</template>
<script setup>
import { ref } from 'vue'
import { AsyncReview } from '@/components/ProductReview'
const product = ref({
name: 'Vue 3 超級筆記本',
description: '結合最先進的前端技術…'
})
function reloadReview() {
// 重新渲染 AsyncReview,會觸發 loader 再次執行
// 這裡只需要重新掛載組件即可
}
</script>
總結
defineAsyncComponent是 Vue 3 官方提供的懶載入 API,能讓我們把不立即需要的元件拆分成 非同步,減少首屏 bundle 大小,提升效能與使用者體驗。- 透過 loadingComponent、errorComponent、delay、timeout、onError 等選項,我們可以在載入過程中提供即時回饋,並在錯誤時給予重試機制。
- 與 Vue Router、UI 套件、SSR 等環境結合時,需要注意 分塊策略、錯誤處理 以及 預載(prefetch) 的時機。
- 常見陷阱包括忘記設定
delay、timeout、或是 loader 回傳非 ES Module,透過最佳實踐(適度切分、提供 UI、命名分塊、配合 prefetch)即可避免。
掌握非同步元件的使用,能讓你的 Vue 3 應用在 效能、可維護性 以及 使用者體驗 上都更上一層樓。現在就把這些技巧應用到實際專案中,感受載入速度的明顯提升吧!祝開發愉快 🎉