van-empty 组件实战:从空白页到友好空状态的设计与调优 做过移动端 H5 开发的朋友十有八九都跟空白页battle 过。接口没数据、列表懒加载失败了、搜索没结果……页面一旦白得干干净净用户下一个动作大概率就是直接划走。我自己早期做项目时就靠几个 if 判断硬扛空数组就显示一条灰灰的文字再不行放个暂无数据——那体验叫一个糙。后来换用 vant 里的状态页组件 van-empty才算是把这块短板补上了。这篇文章就围绕 van-empty 的使用场景、关键参数、定制方式和常见坑位把实际项目里怎么把它用顺讲透适合正在用 vant 做移动端页面、又不想在空状态上浪费太多时间的开发者。1. 先说清楚空状态到底解决什么问题1.1 空白页的隐性成本很多人觉得空状态就是个兜底 UI随便 show 一下就行但实际上它是用户流失的重灾区。想象一下用户刚从首页进到订单列表接口一顿加载最后屏幕上什么都没有。他分不清是手机卡了、接口报错了还是自己压根没下过单。这时候他大概率会刷新、退出、甚至直接卸载。这不是危言耸听移动端页面退出率最高的几个节点里不明所以的空白页常年上榜。van-empty 的核心价值不是把空状态补充上而是给用户一个继续操作的出口。它的默认插槽就是给你放按钮用的去逛逛重新加载去充值……配合一句提示文案用户就明白自己是处在什么状态、接下来能做什么。这背后其实是交互设计里的状态可见性原则——系统任何时刻都要让用户知道发生了什么。1.2 van-empty 在团队里的定位在 vant 组件库里van-empty 的存在感不算高属于平时想不起来、一遇到空数据就得翻文档的那类组件。但它确实是提升页面完成度的关键拼图。实际项目里几乎没有哪个列表页敢说自己永远有数据购物车可能被清空、收藏夹可能全取消、消息通知可能一条没有。这些场景用一个统一的组件去承接比每个人手写一套 div img p 要省心太多。我用下来的体会是组件虽小但对整洁度和一致性的贡献很大。只要团队统一用 van-empty空状态的风格就是一致的不会出现 A 页面是张图片、B 页面是一行灰字、C 页面干脆什么都没有的乱象。一方面省了设计稿的沟通成本另一方面代码审查时也能少操不少心。2. 基础使用从安装到渲染出第一屏空状态2.1 引入组件如果你用的是完整引入直接注册即可import { createApp } from vue; import Vant from vant; import vant/lib/index.css; const app createApp(App); app.use(Vant);如果你项目里是按需引入我更推荐这种用官方推荐的插件方式import { Empty } from vant; app.use(Empty);如果用的是 Vant 4组件会自动通过 tree-shaking 按需打包不用担心体积问题。这里有个容易踩的小坑Empty这个组件在 vant 里的注册名是VanEmpty模板里用van-empty不少刚从 React 切过来的朋友会下意识写成van-empty /以外的形式其实 Vue 单文件组件里自闭合没问题但要注意大小写van-Empty是没法正常解析的。2.2 第一个空状态页模板里最简写法就三行van-empty description这里空空如也 /这句代码渲染出来的就是默认的占位图加一行描述文字。默认图片是 vant 内置的一张插画风格的灰色底图不算惊艳但胜在规整。我刚开始用的时候在想description 为什么是 props 而不是 slot后来才想明白大部分空状态都只需要一行文案做成 props 是最省事的写法。如果你需要在描述下方塞按钮或者自定义布局再用插槽也不迟。description属性默认值是空字符串也就是说你不传的话页面上只有图没有字。很多新手会疑惑为什么我的空状态只有一张图八成就是忘了传 description或者传了但值是空的。2.3 核心 props 速查表van-empty 的 props 不多但有几个是高频使用的这里整理成表格方便查阅。属性名说明类型默认值image图片 URL 或图片地址string默认占位图image-size图片的宽高支持 number/string/arraynumber | string | [number, number]未设置description描述文字string空字符串image设置为空字符串时显示自定义插槽图片--比如你想把图片从默认 120 x 120 调大一些可以这样van-empty description还没有收藏任何商品 image-size160 /image-size传单个数值时会把宽高设为相同的值传数组的话可以分别控制宽高van-empty description数据加载失败 :image-size[200, 100] /实际项目里我很少用数组形式只有在视觉上需要横构图的时候才用比如失败页放一张宽一点的插图。多数场景一个数值就够了。3. 业务场景实战不同空数据显示不同的引导3.1 购物车为空给用户一个去逛的理由购物车空状态是移动电商里最典型的 van-empty 应用场景。这个场景的套路是图片 购物车还是空的文案 去逛逛按钮。按钮点击后跳转到首页或者商品列表页。van-empty imagehttps://cdn.example.com/cart-empty.png description购物车还是空的 van-button round typedanger sizesmall clickgoShopping 去逛逛 /van-button /van-empty这里有个细节默认插槽里放 button 的时候按钮会和 description 之间形成垂直排列的布局间距是 vant 内部算好的。如果你需要自定义间距我建议用style包裹一层而不是直接去改组件内部样式——改内部样式的代价是你升级 vant 版本时大概率要重新调。按钮的类型选择上我习惯用typedanger或者品牌主色。移动端空状态的按钮不要弄太大sizesmall就够太大会让空状态区域变得很重。另一个小技巧是按钮文案用去逛逛而不是确定动作指引的意义更强。3.2 订单列表为空区分未登录和真没有订单列表的空状态比购物车复杂一点因为存在两种截然不同的情况用户还没登录和用户真的没有订单。这两种情况的引导路径是不一样的。未登录状态下一般做法是先弹登录框或者跳登录页。这个场景我一般不会用 van-empty 的默认插槽放按钮因为按钮跳转逻辑不一样我会直接用v-if控制整套空状态的展示配合登录按钮单独处理van-empty v-if!isLogin description登录后查看订单记录 van-button round sizesmall clickshowLogin true 去登录 /van-button /van-empty van-empty v-else-iforderList.length 0 description暂无订单去下一单吧 van-button round sizesmall clickgoHome 随便逛逛 /van-button /van-empty用v-if/v-else-if而不是v-show是因为这两个状态组件的图片和逻辑都不同频繁切换时v-show会同时挂两棵 DOM 树反而浪费。订单页这种场景切换频率不高v-if是最合适的。3.3 搜索无结果给用户留一条修正路搜索无结果的空状态最容易踩的坑是文案太干。什么没有找到相关内容——用户看了依然不知道怎么办。比较好的做法是描述里说明换个关键词试试然后放一个清除筛选按钮或者直接展示热门搜索词。van-empty imagehttps://cdn.example.com/search-empty.png description没有找到相关商品换个关键词试试 van-button round sizesmall clickresetKeyword 清除筛选 /van-button /van-empty从交互角度说搜索无结果页最重要的不是展示状态而是降低用户的操作成本。用户已经输入了关键词他的预期是看到结果结果页就要给他一条回头路。所以按钮最好放在首屏能直接看到的位置不要藏在滚动区域下面。3.4 网络异常与加载失败重试逻辑怎么接很多项目里接口失败和空数据是混在一起的导致用户看到暂无数据时以为是自己没操作对搞不清是网络问题还是真没数据。我的习惯是把接口失败和空数据分两个状态分别处理。接口失败用 van-empty 配一张失败图放重新加载按钮van-empty imagehttps://cdn.example.com/error.png description加载失败检查网络后重试 van-button round sizesmall clickretryFetch 重新加载 /van-button /van-empty重试按钮绑定的方法里要把 loading 状态一并处理免得用户连点出并发请求const loading ref(false); async function retryFetch() { if (loading.value) return; loading.value true; try { await fetchOrderList(); } catch (err) { // 错误处理 } finally { loading.value false; } }这个模式几乎适用于所有带重试的空状态页建议直接封装成一个useLoadFallbackcomposable各页面复用代码能清爽很多。4. 定制与扩展把默认组件做出设计感4.1 自定义图片传 URL 还是用插槽自定义图片有两条路image属性传 URL或者用image插槽把整个图片节点替换掉。两条路的使用场景完全不同。image属性适合那种只需要换一张图、其他都保持默认的情况van-empty imagehttps://cdn.example.com/custom-empty.png description暂无优惠券 /image插槽适合图片需要动态处理、加动画、或者加载本地 SVG 资源的情况。比如你用require动态引入图片时插槽会更直观van-empty description暂无优惠券 template #image img src/assets/empty-coupon.svg altempty / /template /van-empty注意插槽里放img时图片尺寸不会再被image-size控制你需要自己在插槽里用 CSS 或属性限定宽高。我见过有同事在这个坑里纠结一下午其实是把两个负责不同层面的 API 搞混了。4.2 默认插槽按钮、副文案、补充内容的放置区默认插槽也就是组件内容区是空状态页面里自由度最高的地方。除了放按钮还能放自定义的副文案、链接、或是一组引导卡片。van-empty description暂无收藏商品 p classempty-tip收藏你喜欢的商品稍后回来看看/p van-button round sizesmall clickgoShopping去首页看看/van-button /van-empty有一点要特别注意默认插槽里的内容默认是水平居中布局如果需要做左对齐或者更复杂的排列要么在外面套一层自定义 class要么用deep改组件内部样式。我个人强烈建议套一层容器比如加个div.empty-actions所有布局都在这层里做不要动组件本身的样式。4.3 与骨架屏、loading 组件的联动空状态并不是页面最初的状态。用户在数据加载过程中看到的是骨架屏或者 loading加载完才发现是空数据。这里有一个常见的时序问题loading 结束的瞬间如果数据是空的直接切到 van-empty 会有一瞬间的闪烁。解决闪烁的办法是让 loading 的容器和 van-empty 共用一个父容器的高度或者让 van-empty 出来后做一个简单的过渡动画。vant 支持transition内置动画但 van-empty 本身没有带动画我这边常用的是在组件外层包一个transition namefade让空状态淡入视觉上会柔和很多。transition namefade van-empty v-if!loading list.length 0 description暂无数据 / /transition.fade-enter-active { transition: opacity 0.3s; } .fade-enter-from { opacity: 0; }这个经验是从实际用户反馈里发现的有用户说数据空的时候页面闪了一下加个过渡动画之后肉眼感知明显降低了。4.4 全局默认图配置建立团队的空状态规范如果你的团队有统一的设计风格可以给 van-empty 提供一个默认的全局图片配置避免每个页面都重复传image属性。做法是在项目入口处配置 vant 的全局组件配置import { Empty } from vant; Empty.setDefaultOptions({ image: https://cdn.example.com/global-empty.png });这样所有页面的van-empty在没有显式传image时会统一用这张全局图。这个功能在 vant 4 里是通过插件提供的默认配置实现的。团队里统一了全局图之后产品的视觉一致性会上一个台阶而且后续如果要换插画风格只需要改一处配置。5. 踩坑记录与排查速查5.1 图片加载不出来先分清是路径问题还是组件问题van-empty 的图片不显示最常见的三个原因图片路径写错、图片资源没打包进产物、以及跨域问题。图片路径这块在 Vue 里如果你写的是相对路径比如image../../assets/empty.png有时候打包后路径会失效。更稳妥的做法是用require或import的方式拿图片地址import emptyImg from /assets/empty.png; // 模板里 van-empty :imageemptyImg description暂无数据 /这样 Webpack 或 Vite 会把图片当成模块处理自动处理路径和 hash不会出现引用错乱。跨域的问题发生在图片放在 CDN 上、且该 CDN 不允许跨域读取的情况下。这种问题表面上是图裂了实际上控制台会报 CORS 错误。排查时先看控制台其次再看网络面板里图片请求的状态码能省不少冤枉时间。5.2 样式被覆盖作用域 CSS 与组件内部样式冲突有同事遇到过这种情况在页面里写了scoped样式想调 van-empty 的间距结果怎么都不生效。这是因为 vant 组件内部的类名会被 scoped 属性处理你的样式选择器作用域不够。解决方法是用深度选择器:deep():deep(.van-empty__description) { margin-top: 12px; color: #999; }注意vant 4 的类名结构在不同版本间可能微调升级前最好看一眼产物里的实际类名。我见过一次升级后van-empty__description变成了van-empty__description van-empty__description--center旧的选择器依然能匹配到但有概率被新类名覆盖所以升级后要多做几轮视觉回归。5.3 空状态被其他元素遮挡层级问题空状态的层级问题多见于页面里同时有 fixed 定位的底部导航或者遮罩层。van-empty 本身没有很高的 z-index如果某天页面里出现空状态被半透明遮罩挡住的情况大概率是你页面上其他元素的 z-index 设置过高。这时候直接给 van-empty 包一层容器设置position: relative; z-index: 1就能解决不建议为了这种事去改 vant 源码。5.4 列表一直加载不出来滚动加载与空状态的状态冲突想一下这个场景你用的是 vant 的van-list做滚动分页加载第一页就返回了 0 条数据。此时列表组件会触发empty事件或者显示空状态但如果你外层同时判断list.length 0渲染了 van-empty就会出现两个空状态叠在一起的情况。我的处理方式是用 van-list 自身的空状态事件去控制 van-empty 的显示而不是在父组件里另写一套判断。van-list v-iflist.length 0 v-model:loadingloading :finishedfinished !-- 列表内容 -- /van-list van-empty v-else description暂时没有数据 /这样由列表是否渲染决定了空状态的显隐不会出现双层重复。如果是老项目里不能简单切分母的就用一个计算属性专门控制空状态显示。5.5 排查速查表问题可能原因建议排查步骤图片不显示路径错误 / 资源未打包控制台看网络请求确认状态码和文件路径描述文字不显示未传 description / 属性值空检查模板里是否传了空字符串自定义按钮排版乱组件默认水平居中在外层包容器控制布局样式不生效scoped 样式无法穿透使用 :deep() 选择器双空状态van-list 与 van-empty 判断重复用 v-if / v-else 互相排他图片太大/太小image-size 未调整设置 image-size 或插槽内限宽高6. 沉淀下来的一些个人经验用 van-empty 的时间长了我越发觉得它是个反直觉的存在——越是看起来简单的组件越需要在使用时想清楚业务的状态边界。状态页不是代码写出来就结束了而是要问自己用户站在这个空状态面前他下一步最自然的动作是什么组件只给了你一张图、一行字、一个插槽真正把引导动作做对要靠业务方和前端一起定。再分享一个最近实践的小习惯把空状态的展示条件收敛成一个computed。页面里的空状态往往不止一个判断维度比如筛选条件为空和筛选结果为空是不同的状态直接在模板里堆v-if会让组件越来越乱用计算属性集中管理后后续接埋点或加引导都方便很多。const showEmpty computed(() { if (loading.value) return false; if (filterApplied.value) { return filteredList.value.length 0; } return rawList.value.length 0; });这个模式我后面基本在所以含空状态的页面上都用了个人觉得是 van-empty 相关实践里最值得推广的一点。说到底空状态是产品给用户的最后一句话与其每次现想不如提前把它设计好。