跳到主要内容

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 的完整内容:

  1. 基本概念:Suspense 的作用
  2. 基本用法:default 和 fallback 插槽
  3. 异步 setup:setup 可以是 async 函数
  4. 路由结合:与 Vue Router 配合使用
  5. 高级用法:超时、嵌套、自定义加载
  6. 实际应用:数据表格、图片画廊
  7. 性能优化:预加载和缓存

练习

  1. 创建一个带加载状态的图片懒加载组件
  2. 实现一个多数据源同时加载的组件
  3. 创建一个无限滚动加载组件

准备好继续深入学习了吗?