Vue 非同步元件
非同步元件(Async Components)是 Vue 提供的一種延遲載入元件的方式。當應用程式很大時,使用非同步元件可以將程式碼分割成較小的區塊,只在需要時才載入,從而提升初始載入速度。
defineAsyncComponent
Vue 3 提供 defineAsyncComponent 函式來定義非同步元件:
<script setup>
import { defineAsyncComponent } from 'vue'
// 基本用法:傳入一個返回 Promise 的函式
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<template>
<AsyncComponent />
</template>
當元件第一次被渲染時,Vue 才會呼叫 import 函式載入元件。
進階選項
defineAsyncComponent 可以接受一個選項物件,提供更多控制:
<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorDisplay from './ErrorDisplay.vue'
const AsyncComponent = defineAsyncComponent({
// 載入元件的函式
loader: () => import('./HeavyComponent.vue'),
// 載入中時顯示的元件
loadingComponent: LoadingSpinner,
// 顯示 loadingComponent 前的延遲時間(毫秒)
delay: 200,
// 載入失敗時顯示的元件
errorComponent: ErrorDisplay,
// 超時時間(毫秒),超過後顯示錯誤元件
timeout: 3000
})
</script>
<template>
<AsyncComponent />
</template>
選項說明
| 選項 | 說明 | 預設值 |
|---|---|---|
loader | 返回 Promise 的載入函式 | 必填 |
loadingComponent | 載入中時顯示的元件 | 無 |
errorComponent | 載入失敗時顯示的元件 | 無 |
delay | 顯示 loadingComponent 前的延遲 | 200ms |
timeout | 超時時間,超過後觸發錯誤 | Infinity |
onError | 錯誤處理函式 | 無 |
錯誤處理
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
errorComponent: ErrorDisplay,
onError(error, retry, fail, attempts) {
// error: 錯誤物件
// retry: 重試函式
// fail: 失敗函式(停止重試)
// attempts: 已嘗試次數
if (error.message.includes('fetch') && attempts <= 3) {
// 網路錯誤,重試最多 3 次
retry()
} else {
// 其他錯誤,或超過重試次數,放棄
fail()
}
}
})
</script>
搭配 Suspense
Vue 3 的 <Suspense> 元件可以更優雅地處理非同步元件的載入狀態:
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent(() =>
import('./Dashboard.vue')
)
</script>
<template>
<Suspense>
<!-- 預設插槽:非同步內容 -->
<template #default>
<AsyncDashboard />
</template>
<!-- fallback 插槽:載入中顯示 -->
<template #fallback>
<div class="loading">載入中...</div>
</template>
</Suspense>
</template>
Suspense 的優勢
- 統一管理:可以在一個地方處理多個非同步元件的載入狀態
- 搭配 async setup:可以在 setup 中使用 await
- 嵌套支援:支援嵌套的非同步依賴
<!-- 多個非同步元件 -->
<template>
<Suspense>
<template #default>
<div>
<AsyncHeader />
<AsyncSidebar />
<AsyncContent />
</div>
</template>
<template #fallback>
<div>載入頁面中...</div>
</template>
</Suspense>
</template>
async setup
搭配 <Suspense>,元件可以有 async 的 setup:
<!-- AsyncComponent.vue -->
<script setup>
// 可以直接使用 await
const response = await fetch('/api/data')
const data = await response.json()
</script>
<template>
<div>{{ data }}</div>
</template>
<!-- 父元件 -->
<template>
<Suspense>
<AsyncComponent />
<template #fallback>載入中...</template>
</Suspense>
</template>
實際應用
路由級別的程式碼分割
搭配 Vue Router 實現路由級別的程式碼分割:
// router.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
},
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
})
條件載入
只在需要時載入特定元件:
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const showChart = ref(false)
// 只有在 showChart 為 true 時才會載入
const ChartComponent = defineAsyncComponent(() =>
import('./Chart.vue')
)
</script>
<template>
<button @click="showChart = true">顯示圖表</button>
<ChartComponent v-if="showChart" />
</template>
Modal 對話框
Modal 通常不需要在頁面載入時就載入:
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const showModal = ref(false)
const HeavyModal = defineAsyncComponent({
loader: () => import('./HeavyModal.vue'),
loadingComponent: {
template: '<div class="modal-loading">載入中...</div>'
}
})
</script>
<template>
<button @click="showModal = true">開啟</button>
<HeavyModal v-if="showModal" @close="showModal = false" />
</template>
Tab 面板延遲載入
<script setup>
import { ref, defineAsyncComponent, shallowRef, markRaw } from 'vue'
const tabs = [
{
id: 'overview',
label: '總覽',
component: markRaw(defineAsyncComponent(() =>
import('./tabs/Overview.vue')
))
},
{
id: 'analytics',
label: '分析',
component: markRaw(defineAsyncComponent(() =>
import('./tabs/Analytics.vue')
))
},
{
id: 'reports',
label: '報表',
component: markRaw(defineAsyncComponent(() =>
import('./tabs/Reports.vue')
))
}
]
const activeTab = ref('overview')
const currentComponent = computed(() => {
return tabs.find(t => t.id === activeTab.value)?.component
})
</script>
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<Suspense>
<template #default>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>
<template #fallback>
<div class="loading-tab">載入分頁中...</div>
</template>
</Suspense>
</template>
預載入(Prefetch)
在使用者可能需要之前預先載入元件:
<script setup>
import { defineAsyncComponent, onMounted } from 'vue'
// 定義非同步元件
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
// 預載入函式
const preloadHeavyComponent = () => import('./HeavyComponent.vue')
// 滑鼠移入時預載入
function handleMouseEnter() {
preloadHeavyComponent()
}
// 或在 mounted 後預載入
onMounted(() => {
// 使用 requestIdleCallback 在瀏覽器閒置時預載入
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
preloadHeavyComponent()
})
}
})
</script>
<template>
<button @mouseenter="handleMouseEnter" @click="showComponent = true">
顯示元件
</button>
</template>
載入和錯誤元件
建立通用的載入元件
<!-- LoadingSpinner.vue -->
<template>
<div class="loading-spinner">
<div class="spinner"></div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
defineProps({
message: {
type: String,
default: '載入中...'
}
})
</script>
<style scoped>
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #42b883;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
建立通用的錯誤元件
<!-- ErrorDisplay.vue -->
<template>
<div class="error-display">
<div class="error-icon">⚠️</div>
<h3>載入失敗</h3>
<p>{{ message }}</p>
<button v-if="retry" @click="retry">重試</button>
</div>
</template>
<script setup>
defineProps({
message: {
type: String,
default: '無法載入元件,請稍後再試'
},
retry: {
type: Function,
default: null
}
})
</script>
<style scoped>
.error-display {
text-align: center;
padding: 40px;
color: #721c24;
background: #f8d7da;
border-radius: 8px;
}
.error-icon {
font-size: 48px;
}
button {
margin-top: 16px;
padding: 8px 16px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
建立非同步元件工廠函式
// asyncComponentFactory.js
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorDisplay from './ErrorDisplay.vue'
export function createAsyncComponent(loader, options = {}) {
return defineAsyncComponent({
loader,
loadingComponent: options.loadingComponent || LoadingSpinner,
errorComponent: options.errorComponent || ErrorDisplay,
delay: options.delay ?? 200,
timeout: options.timeout ?? 10000,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry()
} else {
fail()
}
}
})
}
使用:
<script setup>
import { createAsyncComponent } from './asyncComponentFactory'
const Dashboard = createAsyncComponent(
() => import('./Dashboard.vue')
)
const Analytics = createAsyncComponent(
() => import('./Analytics.vue'),
{ timeout: 5000 }
)
</script>
效能考量
適度使用:不是所有元件都需要非同步載入,只有較大或不常用的元件才需要
預載入重要路由:使用者可能會訪問的路由可以預先載入
設定合理的 delay:避免快速載入的元件顯示閃爍的載入畫面
使用 Webpack/Vite 的 magic comments:
// 設定 chunk 名稱
const Component = defineAsyncComponent(() =>
import(/* webpackChunkName: "my-chunk" */ './Component.vue')
)
// Vite 中預載入
const Component = defineAsyncComponent(() =>
import(/* @vite-ignore */ './Component.vue')
)