拓冰建站拓冰建站
首页 / 资讯中心 / 正文

uni-app progress组件实战:动态加载与模拟进度封装

uni-app 的 progress 组件说实话用起来不难但真正做到“动态加载”和“模拟进度”的时候坑比想象中多。我前段时间做一个跨端项目需要做启动加载页一开始直接用现成的 UI 库进度条结果在 App 端渲染直接拉胯后来老老实实切回官方 progress 组件自己封装反而把多端兼容问题全解决了。这篇文章我把这套完整的玩法拆开讲清楚从基础属性到模拟加载的实现思路再到封装一个可复用的进度组件最后附上我在实战中踩过的坑希望能帮你少走弯路。不管你是刚开始接触 uni-app 的新手还是已经在做跨端项目的开发者只要你想在页面里展示进度条、做动态加载效果或者需要模拟一个“看起来合理”的加载过程这篇文章都值得你花几分钟看完。1. 先把 progress 组件的家底摸清楚1.1 它解决的到底是个什么问题移动端用户最怕什么最怕点击之后页面毫无反应。不管是提交表单、上传图片还是拉取数据如果界面没有任何反馈用户的第一反应就是“卡死了”然后开始反复点击甚至直接退出。进度条的核心价值就是告诉用户系统还在工作你等一等。uni-app 的 progress 是官方内置组件不需要安装任何第三方依赖直接在 template 里写progress标签就能用。它最大的优势是官方封装、多端统一。微信小程序、支付宝小程序、H5、App 端都走同一套渲染逻辑不用像用第三方 UI 库那样还要单独适配各端表现。这一点在做跨端项目的时候尤其重要进度条这种简单的组件越是通用越不需要引入额外依赖。1.2 核心属性逐个拆解progress 组件的属性数量不多但每一个单独拎出来都有讲究。属性类型默认值说明percentNumber0进度百分比范围 0~100支持小数stroke-widthNumber6进度条线的宽度单位 pxactiveColorString#09BB07已进度的颜色backgroundColorString#EBEBEB未进度的背景颜色activeBooleanfalse是否开启动画过渡activeModeStringbackwards动画模式backwards 或 forwardsshow-infoBooleanfalse是否在右侧显示百分比文字border-radiusNumber/String0进度条圆角大小其中percent是所有逻辑的核心动态加载的本质就是不断修改这个值。stroke-width默认只有 6px比较细在移动端视觉上偏秀气如果要做醒目一点的进度条一般会调到 10 到 16。active和activeMode是很多人容易忽略的坑。active为 true 时进度条会有一个滑动的动画效果但注意它的默认模式是backwards意思是每次更新 percent 时进度条会先回到 0再滑到新位置。如果你是在做连续递增的进度比如从 20% 更新到 40%默认情况下用户会看到进度条“嗖”一下弹回 0再慢慢滑到 40%非常诡异。正确的姿势是显式设置activeModeforwards这样进度条就会从当前位置继续往后滑符合日常认知。show-info也很实用设置 true 后会在进度条右侧自动显示 “66%” 这样的文字省去了自己再单独写一个文本节点。不过要注意这个文字的位置是紧贴在进度条右侧的如果你的进度条宽度不够数字可能被挤到下一行需要合理控制宽高。1.3 原生属性不够用怎么办官方 progress 能定制的东西其实有限比如你想要渐变进度条、条纹进度条、圆头进度条之外的复杂样式直接改官方组件就比较费劲。这时候通常有三种解法方案一直接用activeColor和backgroundColor改颜色简单场景够用。方案二给 progress 加 class用 CSS 覆盖样式但不同端对内部结构的渲染方式不完全一致H5 端可以用::v-deep穿透小程序端有时候改了没效果兼容性一般。方案三干脆自己用 view 嵌套模拟一个进度条外层写背景、内层用width: percent%控制进度配合 CSS transition 做动画三端表现完全一致。我做视觉定制比较重的页面时基本直接走方案三。自己写一个进度条也就二十行代码但可控性高得多想加渐变就加渐变想加图标就加图标不受官方组件内部结构限制。官方 progress 更适合那种快速出效果、视觉要求不高、什么端都要跑的场景。2. 动态加载进度的核心思路为什么是“模拟”2.1 真实进度和模拟进度的边界在哪先厘清一个概念进度条的数据来源分两种一种是真实进度一种是模拟进度。真实进度常见于文件上传、资源下载、导出任务等场景系统能通过回调拿到实际完成量然后换算成百分比。比如 uni.request 上传文件时可以通过onUploadProgress拿到progress事件里面有已上传的字节数除以总字节数就是真实进度。模拟进度则常见于页面初始化、冷启动加载、或者后端一次性返回数据的场景。这类场景的共性是你只知道“请求中”和“请求完成”两个状态中间过程完全黑盒拿不到任何中间进度。这时如果什么都不显示用户只能对着空白页面干等如果显示一个活跃的进度条用户至少知道系统“在干活”。模拟进度的核心价值不是精确而是“给用户一个合理的等待预期”。很多成熟产品根本不给真实进度用一套精心设计的模拟策略撑过整个等待期用户体感反而更好。所以别觉得“模拟”是骗人产品层面它就是正当的体验优化手段。2.2 三种实现方案对比模拟进度的技术实现核心就是定时器配合更新 percent 值。我试过三种方案各有优劣。方案一setInterval 固定间隔递增。this.timer setInterval(() { if (this.percent 100) { clearInterval(this.timer) return } this.percent Math.random() * 5 2 }, 200)这个方案的优点是代码极简一眼就懂。缺点是策略过于固化如果增量不够大进度条会走得很慢如果增量太大一两秒就冲到了 90%后面真实任务没完成进度条就只能停在 99% 尴尬地等。方案二setTimeout 递归每次重新计算延迟和增量。const run () { this.timer setTimeout(() { this.percent this.calcStep() run() }, this.calcDelay()) }这个方案更灵活关键在于可以按当前进度动态调整步长和间隔模拟出“开始快、中间稳、最后慢”的视觉节奏观感最接近真实加载。代价是代码量稍多需要管理递归调用的 timer。方案三requestAnimationFrame 时间戳做缓动动画。只在 H5 端跑的话非常流畅但小程序端对 requestAnimationFrame 支持不够稳定App 端也要看运行环境跨端项目不建议作为主方案。我做跨端项目的推荐是方案二setTimeout 递归加分阶段步长策略兼顾灵活性和兼容性。2.3 分阶段步长策略模拟进度的灵魂模拟进度不能随便写个随机数就完事要符合用户对加载过程的直觉。我实际项目里用的分阶段策略是这样0% ~ 60%快速增长阶段每次加 2~7整体几秒内能看到明显前进。60% ~ 90%增速放缓每次加 0.5~2.5让用户感觉快完成了。90% ~ 99%蜗牛速度每次加 0.1~0.5营造“就差最后一点”的紧张感。99% 封顶停滞等真实任务完成后再跳到 100%。为什么要封顶在 99%因为真实任务可能还在跑如果进度条先走到头用户就会产生“马上就好”的期待结果半天没动静反而催生焦躁情绪。停在 99% 是一种心理暗示已经差不多了再坚持一下。这是产品设计中很常规的“预期管理”手法。另一个细节是进度条永远不要往回走。用户看到进度条 30% 突然掉回 20%第一反应就是“是不是出问题了”。所以模拟策略里要加保护当前进度只增不减任何情况下新值不能小于旧值。3. 完整实操从零封装一个可复用的模拟进度组件3.1 组件结构设计动态加载进度虽然逻辑不复杂但如果每个页面都写一套定时器代码项目很快就会被重复代码淹没。正确的做法是封装成一个独立组件把定时器管理、步长计算、状态控制全部收敛在组件内部页面只调用 start、finish、reset 这几个方法。我封装的组件结构如下template view classsimulate-progress progress :percentpercent :stroke-widthstrokeWidth :activeColoractiveColor :backgroundColorbgColor :show-infoshowInfo :activeactive :activeModeactiveMode / slot/slot /view /templateprops 设计上要兼顾灵活性但不要把底层实现细节暴露给调用方。我常用的 props 配置props: { strokeWidth: { type: Number, default: 8 }, activeColor: { type: String, default: #07c160 }, bgColor: { type: String, default: #ebebeb }, showInfo: { type: Boolean, default: true }, active: { type: Boolean, default: false }, activeMode: { type: String, default: forwards }, autoplay: { type: Boolean, default: false } }这里有个设计决策percent 不放在 props 里由父组件直接控制而是作为组件内部 data。原因很简单模拟进度的时序逻辑由组件自身管理更内聚父组件只要关心“开始、结束、重置”这三个动作不需要理解步长怎么算、定时器怎么排。这符合组件化设计里“低耦合、高内聚”的基本原则。组件事件方面定义两个事件success进度达 100 后触发、error外部主动调用 fail 时触发。这样页面就能通过监听事件做后续跳转或提示不用轮询进度值。3.2 核心逻辑实现与逐行解析组件内部的 data 和方法export default { name: SimulateProgress, data() { return { percent: 0, timer: null } }, mounted() { if (this.autoplay) { this.start() } }, methods: { start() { this.percent 0 this.run() }, run() { this.clearTimer() const step this.calcStep() this.timer setTimeout(() { if (this.percent 100) { this.clearTimer() this.$emit(success) return } this.percent Math.min(this.percent step, 99) this.run() }, this.calcDelay()) }, calcStep() { if (this.percent 60) { return Math.random() * 5 2 } if (this.percent 90) { return Math.random() * 2 0.5 } if (this.percent 99) { return Math.random() * 0.4 0.1 } return 0 }, calcDelay() { if (this.percent 60) return 200 if (this.percent 90) return 300 if (this.percent 99) return 400 return 500 }, finish() { this.clearTimer() this.percent 100 this.$emit(success) }, reset() { this.clearTimer() this.percent 0 }, fail() { this.clearTimer() this.$emit(error) }, clearTimer() { if (this.timer) { clearTimeout(this.timer) this.timer null } } } }核心在 calcStep 和 calcDelay 两个方法。calcStep 负责按当前进度区间返回一个合理的增量calcDelay 负责动态调整下一次更新的时间间隔。0~60% 阶段间隔短、增量大进度条快速前进进入 90% 之后间隔拉长、增量极小进度条看起来像在艰难地向前蠕动。这套组合策略带来的视觉效果在实际体验中非常接近真实加载。代码里每个方法开头都调用 clearTimer这是防止定时器叠加的关键。如果不做这一步用户调整进度时可能出现两个定时器同时运行进度条突然跳出一大截完全不可控。每次创建新定时器前先清掉旧的保证任意时刻只有一个定时器在跑。3.3 在页面里集成页面里使用这个组件很简单simulate-progress refprogress :autoplaytrue successhandleSuccess /然后在页面逻辑层onLoad() { this.loadData() }, methods: { async loadData() { try { const res await this.$api.getList() // 请求成功直接让进度条跳到 100% this.$refs.progress.finish() this.list res.data } catch (e) { // 请求失败触发 error 事件你可以显示重试按钮 this.$refs.progress.fail() } }, handleSuccess() { // 进度完成后执行跳转或其他逻辑 } }我建议在组件里加一个 autoplay 属性让组件在自身 mounted 钩子里自动启动。这样页面不用惦记在 onReady 里手动调用 start也避免了 this.$refs.progress 还没渲染完就被调用的报错问题。像上面这种页面初始化加载场景配 autoplay 最省心。如果你需要根据任务类型动态决定是否启动进度条那就不开 autoplay手动调用 start 即可flexibility 更高。3.4 配合真实接口做混合进度有些场景不能纯用模拟进度比如文件上传用户能感知到真实进度你也确实能拿到真实数据。这时候最好的方案是混合进度前 50% 用模拟进度占位等真实回调来了从 50% 开始切换到真实进度。实现思路是在组件里增加一个 update 方法update(realPercent) { // 只有真实进度大于当前模拟进度时才覆盖 if (realPercent this.percent) { this.clearTimer() this.percent realPercent } }一句话解释这个逻辑模拟进度只是兜底动画一旦真实进度超过它马上把控制权交给真实进度。这样既保证了等待期的观感又保证了数据的真实性不会被用户“识破”。混合进度有个性能问题要留意真实进度回调的频率可能很高比如文件上传事件每 100ms 触发一次如果每次都 setData 更新进度条在低端安卓机上会出现明显卡顿。解决方案是节流设置一个最小更新间隔比如 200ms 内只允许更新一次。我试过 150ms 的阈值还是会掉帧200ms 基本稳了。4. 常见问题与排查技巧实录4.1 进度条不显示先查这三个地方进度条不显示是遇到最多的反馈我排查下来 90% 的情况出在这三处第一percent 的值是不是 null 或者 undefined。很多人把 percent 绑定在一个初始为 null 的变量上进度条会把它当作 0 处理自然什么都看不到。初始化时给个 0 或一个小值就好了。第二父容器高度是不是塌了。progress 是块级元素宽度默认撑满父容器但如果父容器高度为 0或者被 overflow: hidden 裁掉了进度条就看不见。排查时先给父容器加个固定高度比如 100px确认是不是布局问题。第三样式中是否全局修改了 box-sizing。我在项目里遇到过设置了全局 box-sizing: border-box 后小程序端 progress 宽度计算错乱进度条变得非常短甚至不可见。这类问题比较隐蔽排查时可以在 style 里给 progress 显式指定 width: 100%。还有一个容易忽略的点如果 progress 的父容器是 flex 布局且 flex-direction 为 column子元素宽度会被压缩成内容宽度进度条就会变成一条很短的线段。解法是给 progress 加一个 width: 100%或者调整父容器的 flex 方向。4.2 进度条卡住不动可能是这几种情况进度条走到一半不动了先分清是“正常运行到封顶位”还是“逻辑出了问题”。如果卡在 99%那是正常行为因为我们刻意封顶等待真实任务完成后再推进。如果卡在 90% 以下大概率是 calcStep 返回了 0。最常见的就是判断条件写反了比如把if (this.percent 60) return Math.random() * 5 2错写成if (this.percent 60) return Math.random() * 5 2整个阶段的步长全乱了。排查方法很简单在 calcStep 里把 percent 和 step 打到控制台看 step 是否长期为 0。另外也要检查定时器间隔是否设置过小如果间隔只有 10ms页面频繁 setData 会卡到像死机一样进度条看起来反而像没动。一个我踩过的隐蔽坑在小程序端如果进度条组件的更新数据量过大比如一次性 setData 了一整页数据每次进度更新都会触发整页 diff低端机直接卡成 PPT。解法是把进度条抽成独立组件让 setData 只作用于组件自身不影响页面其他区域。4.3 多端差异同一个组件不同端效果不一样uni-app 的 progress 虽然官方说多端一致但实测还是有差异我整理几个典型:H5 端对 active 动画支持最自然activeMode 的 forwards 和 backwards 表现都符合预期。微信小程序在 iOS 上动画流畅但部分安卓机型上如果 stroke-width 设置的特别粗比如 20px 以上active 动画会有明显掉帧。App 端vue2 环境对 active 动画支持最弱有时设置 forwards 实际表现还是直接跳变没有过渡。所以我的建议是如果你的主端是 App不要依赖 active 属性做动画直接走 JavaScript 定时器更新 percent或者干脆自定义进度条配 CSS transition。自定义进度条的核心思路前面提过外层 view 做背景、内层 view 宽度等于百分比、加 transition: width 0.3s ease这套写法在 H5、小程序、App 三端几乎零差异是最稳妥的方案。4.4 内存泄漏组件销毁后定时器还在跑这是封装模拟进度组件最容易埋雷的地方。如果你的组件用了 setInterval而组件在销毁时没有清除定时器定时器就会一直跑下去。在小程序端的表现是页面已经销毁了控制台还在刷 setData 警告在 App 端可能表现为内存持续上涨切换页面越来越卡。解决方案三条缺一不可第一在组件生命周期钩子里清除定时器。Vue2 的 uni-app 用 beforeDestroyVue3 或者 uni-app x 要用 onBeforeUnmount注意别写错了。第二页面 onHide 时暂停进度条。用户切到后台再回来如果进度条已经偷偷到 100%体验很怪。暂停后 onShow 再恢复进度条位置保持不变。第三用 setTimeout 递归时要管理好最新的 timer id。每次创建新定时器前先把旧的清掉我这里是在 start、run、finish、reset 里统一 clearTimer确保任意时刻只有一个定时器在运行。下面是我排查多端计时器释放问题时的总结表场景需要清理的钩子常见错误Vue2 语法小程序beforeDestroy / onUnload只清理了 onUnload组件销毁不触发Vue3 语法 / uni-app xonBeforeUnmount还在用 beforeDestroy清理不生效H5 单页应用beforeUnmount路由切换后定时器还在跑内存泄漏App 端onUnload页面销毁但定时器回调仍在执行统一的做法是组件内部生命周期钩子清理一次页面 onHide 暂停一次onUnload 再兜底清理一次。三层保障基本杜绝定时器泄漏问题。5. 封装进度组件的另外两个小技巧前面的内容已经覆盖了进度组件的核心使用和动态加载的实现最后再分享两个我在实际项目中沉淀下来的小技巧能让你的进度条组件更完善。第一个技巧启动时机刻意延迟一下。页面 onLoad 之后不要立刻启动进度条延迟 100ms 左右再开始。这是为了等页面完成首帧渲染否则进度条组件本身的加载也算在耗时里用户会看到进度条刚走没几步就突然跳到 100%显得很假。加上这 100ms 延迟模拟加载的观感会自然很多。第二个技巧给组件加一个 completeAfter 参数控制进度走到 100% 后延迟多久自动触发 success 事件。很多页面需要在进度条走完后展示下一步按钮或者跳转页面这个参数能让调用方精确控制等待时间。实现很简单在 finish 方法里用 setTimeout 延迟一下再 emit 事件即可。30 到 100 是我实测下来比较舒服的范围。另外如果你想在进度条旁边加点自定义内容比如一个“加载中…”的小图标slot 就是现成的扩展点。官方 progress 只负责画条其他的状态提示、按钮、占位文案都可以通过 slot 放进去组件自身的样式和布局保持干净调用方按需扩展互不干扰。我自己在做这类组件时最大的体会是进度条不是越精确越好而是要贴合用户对等待过程的直觉预期。模拟进度的本质是管理用户的心理时间让等待变得可以接受。把这个思路想清楚你写出来的进度条组件就不只是一段代码而是一个完整的产品体验细节。希望这篇文章能帮你在 uni-app 项目里把 progress 组件用得更顺手。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门