空状态组件设计与实现的实战经验分享
我的写法,亲测靠谱
空状态组件这东西,看着简单,真在项目里用起来,坑可真不少。我最早就是随便搞个 div 套句话完事,后来被产品怼了好几次:这个太丑了、没情感、用户不知道下一步干嘛……折腾了几轮后,现在我做 Empty 的方式基本固定了,分享出来,省得你再踩一遍。
我现在写的 Empty 组件长这样:
<div class="empty-state">
<img src="/assets/empty-box.svg" alt="空状态图标" class="empty-icon" />
<p class="empty-title">暂无数据</p>
<p class="empty-description">当前列表中没有符合条件的内容,请尝试调整筛选条件</p>
<button class="btn btn-primary empty-action" @click="handleRefresh">刷新一下</button>
</div>
.empty-state {
padding: 64px 20px;
text-align: center;
color: #666;
font-size: 14px;
}
.empty-icon {
width: 120px;
height: 120px;
margin-bottom: 16px;
opacity: 0.7;
}
.empty-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.empty-description {
color: #999;
line-height: 1.5;
margin-bottom: 20px;
}
.empty-action {
padding: 8px 16px;
font-size: 14px;
}
这种结构我用了快三年,从 Vue 到 React 都适用。关键点在于:结构清晰 + 语义明确 + 可交互。很多人只把它当展示组件,但我觉得它必须带“出路”——要么引导操作,要么提示原因。
比如上面的 handleRefresh,不只是刷新页面,很多时候是重新拉接口。我一般会把这类逻辑封装成 props 传进来,方便复用。
别再这么写了,我都替你看累了
下面这些写法,我在 Code Review 时见一次劝一次,真的别再用了。
- 纯文字提示:“暂无数据” —— 用户看到就懵了,是不是出错了?还是本来就该这样?啥也不说清。
- 图标太大或太小:有人放个 300px 的图,整个页面都被占满了;也有人用 20px 小 icon,根本看不见。
- 按钮缺失或乱放:比如加了个“新增”按钮,但当前场景根本不支持新增,这不是误导吗?
- 直接用 alert 或 toast 提示“无数据”:这是最离谱的,用户体验极差,关掉还要点一下。
还有一个常见错误是动态渲染时机不对。比如我见过这种写法:
// 错误示范
if (data.length === 0) {
return <Empty />;
}
return <List data={data} />;
问题在哪?如果请求还没回来,data 是 undefined 或 null,这时候就会直接进 Empty,但实际上只是 loading 状态。正确做法是:
if (loading) {
return <Loading />;
}
if (data && data.length === 0) {
return <Empty />;
}
return <List data={data} />;
注意这里判断 data 是否存在。我踩过好几次这个坑,接口失败时 data 没定义,直接 .length 报错,页面白屏。所以一定要先判空。
实际项目中的坑
我们有个管理后台,列表页用的是分页查询。一开始设计是:第一页没数据才显示 Empty,后面翻页没数据就不显示了。结果上线后用户反馈“翻着翻着列表没了”,因为第二页返回空数组,前端以为还有数据,但其实已经到底了。
后来改成了:只要当前页返回空数组,并且不是 loading 状态,就显示 Empty。同时加上一句“已加载全部内容”之类的描述,避免误解。
另一个问题是国际化。早期我们把文案写死在组件里,后来多语言一上,全乱套了。现在我的方案是把文本抽成 props:
<Empty
title="No Data"
description="Try adjusting your filters"
actionText="Refresh"
@action="handleRefresh"
/>
或者更进一步,用 i18n key:
const messages = {
en: {
empty_title: 'No Data',
empty_desc: 'Try adjusting your filters'
},
zh: {
empty_title: '暂无数据',
empty_desc: '请尝试调整筛选条件'
}
}
然后通过 $t('empty_title') 动态渲染。这套流程跑顺了之后,换语言基本不改结构。
还有一点容易忽略:**SEO 和无障碍访问(a11y)**。虽然 Empty 多数出现在 SPA 里,但图标要加 alt,文字要有层次(title 用 h3 或 strong,desc 用普通 p),按钮要有可访问性标签。别小看这些,有些客户真会拿工具扫 accessibility 问题。
要不要加动效?我建议谨慎
有次产品经理非要在 Empty 里加个飘动画的小纸飞机,说是“增加趣味性”。我实装后发现,首屏加载时那个动画会闪一下,体验反而更糟。而且低端安卓机上帧率直接掉到 20fps。
最后妥协方案是:只有首次进入且确实为空时才播放一次动画,后续刷新不再播放。通过 localStorage 记录状态,或者路由参数控制。
所以我的建议是:动效可以有,但必须满足三个条件:
- 不影响首屏性能
- 不会重复干扰用户
- 不是为了炫技而加
否则不如静态图来得稳。
API 设计我也琢磨明白了
我现在封装的 Empty 组件,props 基本长这样:
props: {
type: { type: String, default: 'data' }, // data / search / network 等类型
image: { type: String, default: '' }, // 自定义图片
title: { type: String, required: true },
description: { type: String },
showAction: { type: Boolean, default: false },
actionText: { type: String, default: '操作' },
loading: { type: Boolean, default: false } // 防止和 loading 混淆
}
其中 type 很实用。比如搜索无结果和网络错误,展示内容应该不同。我可以根据 type 自动映射默认文案和图标:
const EMPTY_CONFIGS = {
data: {
image: '/empty-data.svg',
title: '暂无数据',
desc: '当前没有内容'
},
search: {
image: '/empty-search.svg',
title: '未找到结果',
desc: '换个关键词试试?'
},
network: {
image: '/empty-network.svg',
title: '网络异常',
desc: '检查网络后重试',
actionText: '重试'
}
}
这样调用的时候就很简单了:
<Empty v-if="error" type="network" @action="fetchData" />
<Empty v-else-if="list.length === 0" type="data" />
既减少重复代码,又保证一致性。
最后一点:别让 Empty 背锅
有一次线上报警,用户打不开页面,一看是 Empty 组件报错了。查了半天发现是因为某个环境下 require 图片路径失败,导致组件渲染中断。
现在的处理方式是:所有资源都 fallback。比如图片加载失败:
<img
:src="imagePath"
@error="useDefaultImage"
alt="空状态"
/>
methods: {
useDefaultImage(e) {
e.target.src = '/default-empty.png';
}
}
另外,组件本身要足够健壮。即使没传 title,也不能崩。我会加默认值,至少让用户看到“空状态”三个字,而不是一片空白。
以上是我总结的最佳实践
Empty 看似是个小组件,但真要把体验做细,要考虑加载顺序、错误边界、可访问性、多语言、复用性一堆问题。我现在这套写法,经过三四个大项目验证,基本没出过幺蛾子。
当然也不是最优解。比如服务端渲染时的样式隔离、微前端下的资源引用,还有优化空间。但对大多数项目来说,够用、稳定、好维护才是第一位。
以上是我踩坑后的总结,希望对你有帮助。有更好的实现方式欢迎评论区交流。

暂无评论