Vue3 Suspense 完整指南
1. 基本概念
Suspense
是 Vue3 提供的一个内置组件,用于处理异步组件和异步数据加载。它可以在等待异步内容加载完成时显示加载状态,并处理加载过程中可能发生的错误。
1.1 基本语法
<template>
<Suspense>
<!-- 异步内容 -->
<template #default>
<async-component />
</template>
<!-- 加载状态 -->
<template #fallback>
<loading-spinner />
</template>
</Suspense>
</template>
2. 常见使用场景
2.1 异步组件加载
<!-- AsyncPage.vue -->
<script setup>
// 异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
2.2 异步数据获取
<!-- UserProfile.vue -->
<script setup>
import { ref } from 'vue'
// 异步 setup
async function setup() {
const response = await fetch('/api/user')
const user = await response.json()
return { user }
}
const { user } = await setup()
</script>
<template>
<div class="user-profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<!-- 父组件 -->
<template>
<Suspense>
<template #default>
<user-profile />
</template>
<template #fallback>
<skeleton-loader />
</template>
</Suspense>
</template>
2.3 多个异步依赖
<!-- Dashboard.vue -->
<script setup>
// 多个异步数据获取
const userData = await fetch('/api/user').then(r => r.json())
const postsData = await fetch('/api/posts').then(r => r.json())
const analyticsData = await fetch('/api/analytics').then(r => r.json())
</script>
<template>
<div class="dashboard">
<user-info :user="userData" />
<posts-list :posts="postsData" />
<analytics-chart :data="analyticsData" />
</div>
</template>
<!-- 父组件 -->
<template>
<Suspense>
<template #default>
<dashboard />
</template>
<template #fallback>
<div class="loading-dashboard">
<loading-spinner />
<p>加载仪表板...</p>
</div>
</template>
</Suspense>
</template>
3. 高级用法
3.1 错误处理
<!-- ErrorBoundary.vue -->
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((e) => {
error.value = e
return false // 阻止错误继续传播
})
</script>
<template>
<div class="error-boundary">
<template v-if="error">
<div class="error-message">
<h3>出错了!</h3>
<p>{{ error.message }}</p>
<button @click="error = null">重试</button>
</div>
</template>
<template v-else>
<slot></slot>
</template>
</div>
</template>
<!-- 使用错误边界 -->
<template>
<error-boundary>
<Suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<loading-spinner />
</template>
</Suspense>
</error-boundary>
</template>
3.2 嵌套 Suspense
<!-- NestedAsync.vue -->
<template>
<Suspense>
<template #default>
<div class="nested">
<async-parent>
<Suspense>
<template #default>
<async-child />
</template>
<template #fallback>
<p>加载子组件...</p>
</template>
</Suspense>
</async-parent>
</div>
</template>
<template #fallback>
<p>加载父组件...</p>
</template>
</Suspense>
</template>
3.3 动态组件切换
<!-- DynamicAsync.vue -->
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const currentTab = ref('tab1')
const tabs = {
tab1: defineAsyncComponent(() => import('./tabs/Tab1.vue')),
tab2: defineAsyncComponent(() => import('./tabs/Tab2.vue')),
tab3: defineAsyncComponent(() => import('./tabs/Tab3.vue'))
}
</script>
<template>
<div class="tabs">
<button
v-for="(_, tab) in tabs"
:key="tab"
@click="currentTab = tab"
>
{{ tab }}
</button>
<Suspense>
<template #default>
<component :is="tabs[currentTab]" />
</template>
<template #fallback>
<div class="tab-loading">
切换中...
</div>
</template>
</Suspense>
</div>
</template>
4. 实际应用示例
4.1 数据表格组件
<!-- DataTable.vue -->
<script setup>
import { ref } from 'vue'
const props = defineProps({
url: String,
columns: Array
})
async function loadData() {
const response = await fetch(props.url)
const data = await response.json()
return { data }
}
const { data } = await loadData()
</script>
<template>
<table class="data-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
{{ row[col.key] }}
</td>
</tr>
</tbody>
</table>
</template>
<!-- 使用数据表格 -->
<template>
<Suspense>
<template #default>
<data-table
url="/api/users"
:columns="[
{ key: 'name', title: '姓名' },
{ key: 'email', title: '邮箱' },
{ key: 'role', title: '角色' }
]"
/>
</template>
<template #fallback>
<table-skeleton :columns="3" :rows="5" />
</template>
</Suspense>
</template>
4.2 图表组件
<!-- ChartComponent.vue -->
<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
async function initChart() {
// 模拟异步数据加载
const data = await fetch('/api/chart-data').then(r => r.json())
const chart = echarts.init(document.getElementById('chart'))
chart.setOption({
// 图表配置
series: [
{
type: 'line',
data: data
}
]
})
return { chart }
}
const { chart } = await initChart()
onMounted(() => {
window.addEventListener('resize', () => {
chart.resize()
})
})
</script>
<template>
<div id="chart" style="width: 100%; height: 400px;"></div>
</template>
<!-- 使用图表组件 -->
<template>
<div class="dashboard-chart">
<Suspense>
<template #default>
<chart-component />
</template>
<template #fallback>
<div class="chart-loading">
<loading-spinner />
<p>加载图表数据...</p>
</div>
</template>
</Suspense>
</div>
</template>
5. 最佳实践
5.1 组件设计
// 将异步逻辑抽离到组合式函数
export function useAsyncData(url) {
return new Promise(async (resolve) => {
const data = await fetch(url).then(r => r.json())
resolve(data)
})
}
// 在组件中使用
const data = await useAsyncData('/api/data')
5.2 加载状态管理
<!-- 提供更细粒度的加载状态 -->
<template>
<Suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div class="loading-state">
<loading-spinner />
<loading-progress :progress="loadingProgress" />
<p>{{ loadingMessage }}</p>
</div>
</template>
</Suspense>
</template>
6. 注意事项
- 避免无限循环
// ❌ 错误示例
async function setup() {
const data = ref(null)
data.value = await fetchData() // 可能导致无限循环
return { data }
}
// ✅ 正确示例
const data = await fetchData()
- 合理的超时处理
<script setup>
import { ref, onMounted } from 'vue'
const timeout = ref(false)
onMounted(() => {
setTimeout(() => {
timeout.value = true
}, 5000)
})
</script>
<template>
<Suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<div>
<loading-spinner />
<p v-if="timeout">
加载时间过长,请检查网络连接
</p>
</div>
</template>
</Suspense>
</template>
- 资源清理
<script setup>
import { onUnmounted } from 'vue'
const controller = new AbortController()
const data = await fetch('/api/data', {
signal: controller.signal
})
onUnmounted(() => {
controller.abort() // 取消未完成的请求
})
</script>