Vue Suspense
Suspense 是 Vue 3.2+ 引入的特殊组件,用于处理异步组件的加载状态。
基本概念
什么是 Suspense?
Suspense 允许你在等待异步组件加载时显示 fallback 内容:
┌─────────────────────────────────────────────────────────┐
│ Suspense 工作流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ <Suspense> │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ #default │ │ │
│ │ │ 异步组件加载完成后显示 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ #fallback │ │ │
│ │ │ 加载中显示此内容 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
基本用法
简单示例
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
异步组件 setup
<!-- UserProfile.vue -->
<script setup>
import { ref } from 'vue'
const props = defineProps({
userId: String
})
// setup 可以是 async 函数
const user = ref(null)
async function loadUser() {
const response = await fetch(`/api/users/${props.userId}`)
user.value = await response.json()
}
loadUser()
</script>
<template>
<div v-if="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
<div v-else>加载中...</div>
</template>
与路由结合
路由懒加载
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
]
在路由视图中使用 Suspense
<!-- App.vue -->
<template>
<nav>
<RouterLink to="/">首页</RouterLink>
<RouterLink to="/about">关于</RouterLink>
</nav>
<Suspense>
<template #default>
<RouterView />
</template>
<template #fallback>
<div class="loading">页面加载中...</div>
</template>
</Suspense>
</template>
<style scoped>
.loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
</style>
高级用法
带超时的加载
<template>
<Suspense>
<template #default>
<AsyncData />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// 带延迟和超时的异步组件
const AsyncData = defineAsyncComponent({
loader: async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
return import('./components/DataList.vue')
},
delay: 200, // 延迟显示加载状态
timeout: 5000 // 超时时间
})
</script>
自定义加载组件
<!-- LoadingSpinner.vue -->
<template>
<div class="spinner-container">
<div class="spinner"></div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
defineProps({
message: {
type: String,
default: '加载中...'
}
})
</script>
<style scoped>
.spinner-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top-color: #42b883;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<template>
<Suspense>
<template #default>
<HeavyComponent />
</template>
<template #fallback>
<LoadingSpinner message="正在加载数据..." />
</template>
</Suspense>
</template>
错误处理
<template>
<Suspense>
<template #default>
<ErrorBoundary>
<AsyncComponent />
</ErrorBoundary>
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
多个异步依赖
并行加载
<template>
<Suspense>
<template #default>
<div>
<AsyncUser />
<AsyncPosts />
<AsyncComments />
</div>
</template>
<template #fallback>
<LoadingSpinner message="加载所有数据..." />
</template>
</Suspense>
</template>
嵌套 Suspense
<template>
<Suspense>
<template #default>
<ParentComponent />
</template>
<template #fallback>
<PageLoader />
</template>
</Suspense>
</template>
<!-- ParentComponent.vue -->
<template>
<div>
<h1>父组件</h1>
<Suspense>
<template #default>
<ChildComponent />
</template>
<template #fallback>
<div>加载子组件...</div>
</template>
</Suspense>
</div>
</template>
实际应用
数据列表加载
<!-- DataTable.vue -->
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const props = defineProps({
endpoint: String
})
const DataRows = defineAsyncComponent({
loader: async () => {
const response = await fetch(props.endpoint)
const data = await response.json()
return {
setup() {
return { rows: ref(data) }
},
template: `
<table>
<thead>
<tr>
<slot name="header" />
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<slot name="row" :row="row" />
</tr>
</tbody>
</table>
`
}
}
})
</script>
<template>
<Suspense>
<template #default>
<DataRows>
<template #header>
<th>ID</th>
<th>名称</th>
</template>
<template #row="{ row }">
<td>{{ row.id }}</td>
<td>{{ row.name }}</td>
</template>
</DataRows>
</template>
<template #fallback>
<div class="loading">
<div class="spinner"></div>
<p>加载表格数据...</p>
</div>
</template>
</Suspense>
</template>
图片画廊
<template>
<Suspense>
<template #default>
<ImageGrid :images="images" />
</template>
<template #fallback>
<div class="placeholder-grid">
<div v-for="i in 9" :key="i" class="placeholder"></div>
</div>
</template>
</Suspense>
</template>
<style scoped>
.placeholder-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.placeholder {
aspect-ratio: 1;
background: #f0f0f0;
border-radius: 8px;
}
</style>
性能优化
预加载
<script setup>
import { ref, onMounted } from 'vue'
const showComponent = ref(false)
onMounted(() => {
// 用户交互前预加载
setTimeout(() => {
showComponent.value = true
}, 100)
})
</script>
<template>
<Suspense v-if="showComponent">
<template #default>
<HeavyComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
缓存组件
import { defineAsyncComponent } from 'vue'
const cachedComponent = null
const getAsyncComponent = () => {
if (cachedComponent) return Promise.resolve(cachedComponent)
return import('./HeavyComponent.vue').then(module => {
cachedComponent = module.default
return cachedComponent
})
}
const AsyncComponent = defineAsyncComponent(getAsyncComponent)
注意事项
SSR 支持
Suspense 在服务端渲染时有特殊处理:
// ssr.js
import { renderToString, Suspense } from 'vue/server-renderer'
// Suspense 会自动处理异步组件
与 Teleport 结合
<Teleport to="body">
<Suspense>
<template #default>
<AsyncModal />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</Teleport>
小结
本章我们详细学习了 Vue Suspense 的完整内容:
- 基本概念:Suspense 的作用
- 基本用法:default 和 fallback 插槽
- 异步 setup:setup 可以是 async 函数
- 路由结合:与 Vue Router 配合使用
- 高级用法:超时、嵌套、自定义加载
- 实际应用:数据表格、图片画廊
- 性能优化:预加载和缓存
练习
- 创建一个带加载状态的图片懒加载组件
- 实现一个多数据源同时加载的组件
- 创建一个无限滚动加载组件
准备好继续深入学习了吗?