一、前言
011 篇埋过伏笔:"<script setup> 顶层裸 await 会白屏,Suspense 032 篇细讲"------今天兑现。本篇两条线:一是 defineAsyncComponent 异步组件 ,让重型组件不拖累首屏;二是 Suspense,给异步组件配统一的"加载中/出错了"外壳。学完这篇,首屏优化和异步加载就都有了标准答案。
二、defineAsyncComponent:用到才加载
重型组件(图表、富文本)在页面加载时就全部打包,首屏白等。defineAsyncComponent 把它变成"用到时才去下载":
vue
<!-- App.vue -->
<template>
<button @click="showChart = true">打开图表</button>
<!-- 点击后才真正下载 BigChart.vue 的代码 -->
<BigChart v-if="showChart" />
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
// 用一个"返回 import() 的函数"定义异步组件
const BigChart = defineAsyncComponent(() =>
import('./components/BigChart.vue')
)
const showChart = ref(false)
</script>
原理一句话:import('./components/BigChart.vue') 是动态导入,Vite 会把它单独拆成一个 js 文件(代码分割),首次渲染 BigChart 组件时才发起网络请求加载。首页的包里没有它,首屏自然快。
【截图位置:F12 Network 里点击后才出现 chart 组件 js 请求的截图】
三、带加载态与错误态的异步组件
网络慢时用户看什么?defineAsyncComponent 支持完整配置:
vue
<!-- LoadingSpin.vue -->
<template><p class="loading">⏳ 组件加载中...</p></template>
<style scoped>
.loading { color: #999; padding: 20px; }
</style>
vue
<!-- LoadError.vue -->
<template><p class="err">❌ 加载失败,请检查网络后重试</p></template>
<style scoped>
.err { color: #e74c3c; padding: 20px; }
</style>
vue
<!-- App.vue -->
<template>
<BigChart />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpin from './components/LoadingSpin.vue'
import LoadError from './components/LoadError.vue'
const BigChart = defineAsyncComponent({
loader: () => import('./components/BigChart.vue'),
loadingComponent: LoadingSpin, // 加载中显示
errorComponent: LoadError, // 失败显示
delay: 200, // 200ms 内加载完就不闪 loading(防抖体验)
timeout: 5000 // 5 秒超时,切到 errorComponent
})
</script>
delay: 200 是个贴心的体验细节:网络快时 loading 一闪而过反而像 bug,200ms 内加载完成就完全不显示。
四、Suspense:给异步 setup 一个统一外壳
另一种异步形态:组件内部顶层 await(等接口数据再渲染)。011 篇说过裸写会白屏------因为父组件不知道它还没准备好。Suspense 就是官方的"等待外壳":
vue
<!-- src/components/UserProfile.vue:顶层 await 的异步组件 -->
<template>
<div class="profile">
<h4>{{ user.name }}</h4>
<p>{{ user.bio }}</p>
</div>
</template>
<script setup>
// ⚠️ 顶层 await:必须配合 Suspense 使用
const user = await new Promise((resolve) => {
setTimeout(() => resolve({ name: '小明', bio: '三年前端' }), 1500)
})
</script>
vue
<!-- App.vue -->
<template>
<Suspense>
<!-- default 插槽:真正的内容(内部只能放一个根节点) -->
<UserProfile />
<!-- fallback 插槽:等待期间显示的占位 -->
<template #fallback>
<p>资料加载中...</p>
</template>
</Suspense>
</template>
<script setup>
import UserProfile from './components/UserProfile.vue'
</script>
执行流程:挂载 → UserProfile 的 setup 开始 await → Suspense 检测到"没就绪"→ 显示 fallback → 1.5 秒后 await 完成 → 切换显示真正内容。
注意实验状态:Suspense 至今仍是实验性 API,行为可能微调,生产环境用前评估;但它解决"顶层 await"这个问题没有替代品,理解思想即可。
【截图位置:fallback 占位与加载完成后内容的切换效果】
五、Suspense 配合错误处理
异步也可能失败,Suspense 体系的标准姿势:外层 onErrorCaptured 捕获 + 默认插槽里 v-if 切换:
vue
<!-- App.vue(错误处理版) -->
<template>
<Suspense v-if="!failed" @fallback="state = 'loading'">
<UserProfile @error="failed = true" />
<template #fallback>
<p>资料加载中...</p>
</template>
</Suspense>
<p v-else>❌ 资料加载失败,请刷新重试</p>
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue'
import UserProfile from './components/UserProfile.vue'
const failed = ref(false)
// 捕获后代组件(含异步 setup)抛出的错误
onErrorCaptured((err) => {
console.error('捕获到异步组件错误:', err)
failed.value = true
return false // 阻止继续向上抛
})
</script>
对应地,UserProfile 里抛错(模拟接口失败):
js
const user = await new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('接口 500')), 1500)
})
错误链路:子组件 reject → Suspense 边界 → onErrorCaptured 捕获 → v-if 切到错误提示。真实项目的错误兜底骨架就是这样。
六、两条线的分工与配合
| 方案 | 解决什么 | 适用 |
|---|---|---|
| defineAsyncComponent | 按需下载组件代码,减首屏体积 | 图表、编辑器等重组件 |
| Suspense | 等待异步 setup,统一加载/错误外壳 | 顶层 await 的数据组件 |
两者可以叠:异步组件 + 内部 await + 父级 Suspense 都成立。另外路由篇(050)的"路由懒加载"用的就是今天第二节同款 () => import()------这里学会,那里白捡。
七、踩坑记录
- 顶层裸 await 没包 Suspense:组件和它以上整棵树都不渲染(白屏)------顶层 await 与 Suspense 是绑定关系,二选一不用就都别用
- Suspense default 插槽放了多个根 :行为未定义,只放一个组件;多个组件各自包一层再组合
- defineAsyncComponent 写成了
defineAsyncComponent(import('...')):少了箭头函数!必须传"函数",让 Vue 决定什么时候调用------() => import(...) - delay 设 0 导致 loading 闪烁:本地网络毫秒级加载完,loading 一闪像 bug------给 200ms 左右的缓冲
- onErrorCaptured 忘了返回 false:错误继续向上抛到全局,控制台红字刷屏------处理后返回 false 静默
八、今日小结
- defineAsyncComponent +
() => import():代码分割按需加载,delay/timeout/loading/error 四件套配置 - Suspense:等待顶层 await,#default 放内容、#fallback 放占位(实验性 API)
- onErrorCaptured 捕获异步错误,返回 false 停止上抛
- 首屏优化第二课完成(第一课是 014 浅层响应式),路由懒加载提前解锁
下篇预告
组件 API 清单还剩最后几个"暗器":透传属性 $attrs、拿到 DOM 和组件实例的 ref、子组件开门 defineExpose。下一篇 033 attrs 与 refs------组件通信的收官之战。