Vue 3 Suspense 與非同步元件深度解析
Vue 3 Suspense 與非同步元件深度解析
非同步資料載入是前端開發中的家常便飯。過去我們習慣用 v-if 搭配 loading 狀態變數來處理,但 Vue 3 的 <Suspense> 元件提供了一種更優雅、更宣告式的解決方案。這篇文章將深入探討 Suspense 的運作原理與實際應用場景。
什麼是 Suspense?
<Suspense> 是 Vue 3 內建的一個特殊元件,它可以在非同步元件載入完成之前顯示備用(fallback)內容。這個概念源自 React,但 Vue 的實作方式有些不同。
基本用法
首先,建立一個使用 async setup() 的元件:
<!-- UserProfile.vue -->
<script setup>
const props = defineProps(['userId']);
// async setup - 這會讓元件成為非同步元件
const response = await fetch(`/api/users/${props.userId}`);
const user = await response.json();
</script>
<template>
<div class="profile">
<img :src="user.avatar" :alt="user.name" />
<h2>{{ user.name }}</h2>
<p>{{ user.bio }}</p>
</div>
</template>接著在父元件中使用 <Suspense>:
<!-- App.vue -->
<template>
<Suspense>
<!-- 預設插槽:非同步元件 -->
<template #default>
<UserProfile :user-id="userId" />
</template>
<!-- fallback 插槽:載入中顯示的內容 -->
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
</div>
</template>
</Suspense>
</template>錯誤處理
<Suspense> 本身不處理錯誤,需要搭配 onErrorCaptured 或 <ErrorBoundary> 元件:
<!-- ErrorBoundary.vue -->
<script setup>
import { ref, onErrorCaptured } from 'vue';
const error = ref(null);
const hasError = ref(false);
onErrorCaptured((err) => {
error.value = err;
hasError.value = true;
return false; // 阻止錯誤繼續向上傳播
});
</script>
<template>
<div v-if="hasError" class="error-boundary">
<h3>載入失敗</h3>
<p>{{ error?.message }}</p>
<button @click="hasError = false">重試</button>
</div>
<slot v-else />
</template><!-- 組合使用 -->
<template>
<ErrorBoundary>
<Suspense>
<template #default>
<UserProfile :user-id="userId" />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</ErrorBoundary>
</template>搭配 Vue Router 的路由層級 Suspense
<!-- layouts/Default.vue -->
<script setup>
import { ref } from 'vue';
const isPending = ref(false);
</script>
<template>
<header>...</header>
<main>
<RouterView v-slot="{ Component }">
<Suspense
@pending="isPending = true"
@resolve="isPending = false"
@fallback="isPending = true"
>
<component :is="Component" />
<template #fallback>
<PageSkeleton />
</template>
</Suspense>
</RouterView>
</main>
</template>搭配 Pinia 的資料預載
// stores/userStore.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const isLoaded = ref(false);
async function fetchUser(id) {
if (isLoaded.value) return;
const response = await fetch(`/api/users/${id}`);
user.value = await response.json();
isLoaded.value = true;
}
return { user, isLoaded, fetchUser };
});<!-- UserDashboard.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore';
const props = defineProps(['userId']);
const store = useUserStore();
// await 會觸發 Suspense
await store.fetchUser(props.userId);
</script>
<template>
<div>{{ store.user?.name }}</div>
</template>Suspense 事件
<Suspense
@pending="onPending" <!-- 開始等待非同步依賴時 -->
@resolve="onResolve" <!-- 非同步依賴完成時 -->
@fallback="onFallback" <!-- fallback 內容顯示時 -->
>
...
</Suspense>嵌套 Suspense
Vue 3.3+ 支援 suspensible prop,允許子元件的非同步操作由最近的父 Suspense 控制:
<Suspense>
<template #default>
<ParentComponent />
<!-- ParentComponent 內部也有 Suspense -->
<!-- 設定 suspensible 讓父層 Suspense 統一管理 -->
</template>
<template #fallback>
<GlobalLoadingSpinner />
</template>
</Suspense>實際案例:Dashboard 頁面
<!-- DashboardPage.vue -->
<script setup>
import { ref } from 'vue';
import StatsWidget from './widgets/StatsWidget.vue';
import ChartWidget from './widgets/ChartWidget.vue';
import RecentActivity from './widgets/RecentActivity.vue';
const loadError = ref(null);
</script>
<template>
<div class="dashboard">
<h1>儀表板</h1>
<!-- 統計數字區塊 -->
<ErrorBoundary>
<Suspense>
<StatsWidget />
<template #fallback>
<StatsSkeleton />
</template>
</Suspense>
</ErrorBoundary>
<!-- 圖表區塊 -->
<ErrorBoundary>
<Suspense>
<ChartWidget />
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
</ErrorBoundary>
<!-- 最近活動 -->
<ErrorBoundary>
<Suspense>
<RecentActivity />
<template #fallback>
<ActivitySkeleton />
</template>
</Suspense>
</ErrorBoundary>
</div>
</template>與傳統 loading 狀態的比較
| 特性 | 傳統 v-if + isLoading | Suspense |
|---|---|---|
| 程式碼結構 | 命令式 | 宣告式 |
| 非同步感知 | 手動管理 | 自動偵測 |
| 巢狀載入 | 複雜 | 簡潔 |
| 錯誤處理 | 需自行實作 | 配合 ErrorBoundary |
| SSR 支援 | 完整 | 部分支援 |
注意事項
<Suspense>目前仍是實驗性 API,介面可能在未來版本中調整- 只有
setup()頂層的await才會被 Suspense 感知,巢狀函數中的await不會 - 配合 SSR 使用時需要特別注意 hydration 的處理
總結
<Suspense> 讓非同步資料載入的程式碼更加優雅,尤其適合需要同時載入多個非同步區塊的頁面。搭配 ErrorBoundary 和 Vue Router,可以打造出媲美原生 App 的流暢載入體驗。雖然它目前仍是實驗性 API,但已相當穩定,值得在正式專案中嘗試。
分享這篇文章