尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Bootstrap 5 加载效果实战:从 spinner 到进度条,打造不焦虑的等待体验

Bootstrap 5 加载效果实战:从 spinner 到进度条,打造不焦虑的等待体验 前阵子给一个后台管理系统做登录页改造测试同学提了个有点尴尬的 bug弱网环境下点击“登录”按钮没反应用户下意识又点了一下结果后端收到两个请求。这个 bug 的根源一点都不复杂——按钮在请求期间没有任何加载反馈用户以为没点上。从那天起我对加载效果这个看起来“只是转个圈”的小事就特别敏感。Bootstrap 5 在这方面其实给了一套相当完整的方案从 spinner 加载图标、按钮 loading 态、进度条到骨架屏占位基本覆盖了大部分前端场景。这篇文章我就结合实际项目经验把 Bootstrap 5 的加载效果从头到尾拆一遍包括组件原理、常见组合方式、真实进度上报、以及我在实战里踩过的坑。不管你是刚接触 Bootstrap 5 的新手还是已经被加载状态反复折腾过的老手这篇应该都能给你一些可以直接抄走的思路。1. 加载效果不止是转圈重复提交和白屏等待的真实代价1.1 两个每天都在发生的糟糕体验先认真聊一个问题加载效果到底是给谁看的表面上是给用户看的告诉系统还在工作。但实际项目中加载状态最大的价值是阻止用户做出错误操作。文章开头那个重复登录的例子就是典型没有 loading 状态用户在等待期间会怀疑系统坏了于是反复点击、刷新页面最后产生一堆重复请求、脏数据甚至重复扣款之类的严重问题。所以加载反馈不是锦上添花它是操作闭环里不可或缺的一环。第二个场景是内容区白屏。页面发起 AJAX 请求到数据返回之间少则几百毫秒多则几秒这块区域如果完全是空白用户的第一反应就是“页面是不是挂了”。一个简单的加载效果能让这段时间从“无限长”变成“可预期”用户的耐心会明显提高。本质上加载效果起到的作用是心理暗示系统还活着请再等一等。1.2 Bootstrap 5 里和加载相关的组件到底有哪些Bootstrap 5 把加载相关的组件拆得比较细大致分成四类每类解决不同层级的问题组件核心类名解决什么问题加载图标spinner-border/spinner-grow单个操作或区域的等待反馈按钮加载btnspinner-*disabled表单提交、操作确认的防重复进度条progress/progress-bar需要展示完成比例的长时间任务占位骨架placeholder/placeholder-glow页面或卡片区域的内容加载预告这四类组件可以单独用也可以自由组合。比如按钮加载形态可以理解成“spinner 嵌入按钮”页面级 loading 可以理解成“spinner 放进遮罩层再挂到 body”骨架屏则可以理解成“用占位符模拟真实内容布局”。理解了这个组合逻辑你在项目中遇到任何特殊加载需求都能用现有零件拼出来不用每次都在网上找现成代码。1.3 为什么新项目别再抠一张 gif 加载图我想先纠正一个习惯就是依赖一张手工做的 loading.gif 图片。早期项目里大家常把一个转圈 gif 放在img标签里用 CSS 控制显示隐藏用了很多年也没觉得有问题。但它有几个硬伤gif 的颜色、尺寸是固定的换主题色就得重新出一张图维护成本高设计师也烦。gif 本质是逐帧动画文件体积不小弱网下它本身可能加载半天。最关键的是它只是一个静态图片资源做不了精细控制比如停止动画、改变速度、局部区域动态切换等都需要额外手段。Bootstrap 5 的 spinner 是纯 CSS 实现的一个 div 加一个类名就出来颜色通过text-*工具类随意切换尺寸通过font-size或spinner-border-sm调整动画速度也可以直接覆盖 CSS 变量。从灵活性、体积、维护成本三个角度看纯 CSS 方案在当前项目里是更合适的选择。后面几章我按场景展开讲这些组件的实际用法。2. spinner 两种形态的底层机制与选择逻辑2.1 border spinner利用 border 生成的圆环与旋转动画Bootstrap 5 的第一个 spinner 形态叫spinner-border视觉上是一个不断旋转的细圆环。它的实现思路如果你手写过 CSS 就能秒懂把元素设成圆形然后利用border的上下左右颜色不同搭出一个“缺了一角”的圆环再让整个元素旋转起来。div classspinner-border rolestatus span classvisually-hiddenLoading.../span /div这一段代码的背后Bootstrap 做了这样几件事通过border-radius: 50%把方块变成圆。设置border: var(--bs-border-width) solid currentcolor同时把右框线设置为透明形成缺口圆环。用keyframes spinner-border把元素从0deg旋转到360deg线性无限循环。有意思的是它默认的文字颜色就是当前文本颜色所以想要改变 spinner 颜色直接加text-primary、text-success、text-danger之类的类名就行不需要额外定制。我用得最多的是text-primary和text-secondary一个适合页面主操作区一个适合弱化背景区域。它还有一个spinner-border-sm小尺寸类专门用在按钮等紧凑场景。如果觉得小尺寸还不够小可以直接设置font-size因为整个 spinner 的宽高是基于em单位的示例代码如下div classspinner-border text-primary stylewidth: 1.2rem; height: 1.2rem; font-size: 0.6rem; rolestatus span classvisually-hiddenLoading.../span /div2.2 grow spinner缩放和透明度叠加的脉冲效果另一种形态叫spinner-grow视觉上是圆点反复“变大变小、变深变浅”比 border 更有存在感常用于需要醒目提示的场景。div classspinner-grow text-primary rolestatus span classvisually-hiddenLoading.../span /div底层的动画原理是两个属性同时变化scale从 0 变化到当前尺寸opacity从 1 逐渐降到 0。这个组合效果很巧妙地模拟了“能量聚集然后消散”的视觉感受。我个人的使用习惯是列表初次加载、页面跳转等待这类“要等一会儿”的场景用 border spinner视觉上更安静、更正式内联操作按钮、局部刷新、强调某个区域正在更新的场景用 grow spinner视觉提醒更明显。两者在代码结构上完全一样想换形态只需要改类名所以在不确定选哪种时先开发再切换成本很低。2.3 尺寸、颜色、多实例组合的那些细节spinner 还支持多实例组合比如三个 grow 小圆圈并排出现模拟常见的“三连点”加载效果。这个在 Bootstrap 里没有内置类但实现起来很轻量只要把多个 spinner 放进 flex 容器里再加上错开的动画延迟div classd-flex gap-2 div classspinner-grow text-primary styleanimation-delay: 0s; rolestatus/div div classspinner-grow text-primary styleanimation-delay: 0.2s; rolestatus/div div classspinner-grow text-primary styleanimation-delay: 0.4s; rolestatus/div /div需要注意一个小细节spinner-grow的动画本身是无限循环的多个实例组合时animation-delay必须是正值不能是负值否则某个实例会直接跳到中间帧视觉上看起来像“卡了一下”。另外多个 spinner 放在同一行时建议用gap控制间距不要手动加 margin后续调布局会省事很多。关于动画速度Bootstrap 5 在源码里定义了--bs-spinner-animation-speed: 0.75s这个变量直接覆盖它就能全局调整速度。比如系统整体走轻盈风格可以在自定义样式里改成0.6s某些图表密集页面希望加载感更沉稳就改成1s。这个变量没有文档专门提但实际非常实用。3. 按钮加载状态从并排图标到“图标在上文字在下”的竖排布局3.1 最标准的按钮 loadingspinner-sm 加文字并排按钮是加载效果最密集的阵地。最常规的做法就是给按钮加一个小的 border spinner按钮本身的文字保留再加disabled防止重复点击。button classbtn btn-primary typebutton idsaveBtn span classspinner-border spinner-border-sm me-2 aria-hiddentrue/span 保存中... /button这里有几个容易被忽略的点aria-hiddentrue要加在 spinner 上因为 spinner 本身是装饰性的提示文字已经表达了“加载中”这个状态读屏用户不该再听到“Loading”之类的重复信息。spinner-border-sm在按钮里几乎是必须的默认尺寸的 spinner 放在默认按钮里会显得很挤视觉重心会乱。me-2是用来在 spinner 和文字之间留空隙的Bootstrap 5 的工具类里已经处理好了间距不需要自己加 margin。这个方案适合文本相对精简的按钮比如“保存”“提交”“登录”。不过我发现一个更稳妥的做法加载中不要替换按钮文字而是保留原有按钮文字只把 spinner 加在文字前面或后面。原因很简单按钮宽度如果因为文字变化而抖动用户视线会被迫移动体验并不好。如果实在想单独表达“提交中”可以给按钮设置一个固定的最小宽度或者用white-space: nowrap防止换行。3.2 竖排布局图标在上文字在下的实现与坑最近看到不少人问“Bootstrap 5 怎么实现图标在上面、文字在下的按钮”这个需求在移动端底部操作栏、上传区域的按钮上非常常见。如果你把div和 flex 布局理解透了其实不需要任何特殊组件Bootstrap 5 的工具类就能解决。button classbtn btn-primary typebutton iduploadBtn stylemin-width: 96px; span classd-inline-flex flex-column align-items-center justify-content-center gap-1 span classspinner-border spinner-border-sm aria-hiddentrue/span span上传中/span /span /button关键点在内部的span上d-inline-flex让它变成一个弹性容器flex-column把主轴方向改成垂直align-items-center保证图标和文字水平居中gap-1控制图标和文字之间的垂直间距。这样按钮内部就形成了“图标在上面、文字在下”的竖排结构。这个写法有两个容易踩的坑。第一个是按钮的高度会被内容撑高如果项目里按钮都有统一高度记得在按钮上手动设置高度或者把按钮内部的 span 设置成d-flex而不是d-inline-flex并让它占满整个按钮。第二个坑是加 spinner 后默认的line-height被撑大导致按钮和相邻元素对不齐建议给这个竖排 span 加一个统一的line-height: 1.2。如果只是要图标和文字上下排列并不需要加载动画那就更简单了把上面代码里的spinner-border换成一个普通图标即可。思路完全一样靠的是 flex 布局而不是某个“竖排按钮专用组件”。3.3 状态切换的完整 JS 逻辑与防重复提交光有 HTML 结构还不够按钮加载态必须和异步请求的生命周期绑定。最基本的逻辑分三步请求发起时禁用按钮并显示 spinner请求结束后恢复按钮请求失败也要恢复。我贴一个完整的示例button classbtn btn-primary typebutton idsaveBtn 保存 /buttonconst btn document.getElementById(saveBtn); const originalHTML btn.innerHTML; async function handleSubmit() { // 1. 进入加载态 btn.disabled true; btn.innerHTML span classspinner-border spinner-border-sm me-2 aria-hiddentrue/span保存中...; try { const res await fetch(/api/save, { method: POST }); if (!res.ok) throw new Error(save failed); // 2. 成功后的业务处理 } catch (err) { // 3. 失败提示 console.error(err); } finally { // 4. 无论成功失败都要恢复按钮 btn.disabled false; btn.innerHTML originalHTML; } } btn.addEventListener(click, handleSubmit);这段代码的核心是finally它保证请求成功、失败、甚至异常中断时按钮都能恢复到可操作状态。很多初学者只在请求成功的分支里恢复按钮一旦接口报错按钮就一直卡在“保存中”的禁用状态用户只能刷新页面这个教训我印象很深刻。另一个升级点是如果业务操作本身很快恢复按钮的瞬间用户几乎察觉不到其实 loading 态也没必要出现。但实践中我们总是趋向于“无脑显示 loading”因为没法预知一个请求会花多久。更稳妥的策略是防抖式 loading后面第 6 章我会专门讲怎么避免闪烁。我还有一个习惯给按钮的加载态加一个数据标记比如btn.dataset.loading true。因为页面里可能有多个入口都触发同一个操作单靠disabled在某些极端操作下还是挡不住比如键盘回车提交表单。加上自定义标记后提交逻辑入口处先判断if (btn.dataset.loading true) return;能更彻底地拦截重复操作。4. 页面级 loading全屏遮罩、骨架屏与并发时序管理4.1 全屏遮罩的最小实现与 z-index 层级控制按钮级别的加载只能覆盖一个操作点但页面初始化、路由切换、上传文件这类场景需要全屏的加载反馈。全屏遮罩通常是自己封装的Bootstrap 5 没有提供现成的 overlay 组件但用工具类和一点自定义 CSS 很容易搭出来。div classloading-overlay idpageLoading div classspinner-border text-primary rolestatus span classvisually-hidden页面加载中.../span /div /div.loading-overlay { position: fixed; inset: 0; z-index: 1050; display: flex; align-items: center; justify-content: center; background-color: rgba(255, 255, 255, 0.85); }这里的inset: 0相当于同时设置top/right/bottom/left: 0让遮罩铺满整个视口。z-index: 1050不是随便写的Bootstrap 5 的模态框z-index是 1055下拉菜单是 1000导航栏是 1020。设置成 1050 可以压住绝大多数页面内容又不会盖过模态框。如果你的 loading 需要覆盖模态框就再往上调到 1056 或更高视具体情况而定。实际项目中这个遮罩一般不会每次都手动渲染而是封装成一个小工具函数const showPageLoading () document.getElementById(pageLoading).classList.remove(d-none); const hidePageLoading () document.getElementById(pageLoading).classList.add(d-none);然后用 CSS 的opacity加transition做一个淡入淡出效果视觉上比生硬地消失柔和不少。更进阶的做法是给遮罩加一个小 logo 或者文字提示比如“数据加载中请稍候”这类细节能让整个产品的专业度提升不少。4.2 用 placeholder 组件搭一套低成本骨架屏页面级 loading 还有一种更高级的表达骨架屏。它不转圈而是用灰色的占位块模拟真实内容布局让用户提前感知到“这里会有一张图、这里会有一行标题”。Bootstrap 5 从 5.1 开始内置了placeholder系列组件使用成本比想象中低很多。div classcard p-3 aria-hiddentrue div classd-flex align-items-center mb-3 div classplaceholder rounded-circle stylewidth: 48px; height: 48px;/div div classms-3 flex-grow-1 div classplaceholder col-6/div div classplaceholder col-4 mt-2/div /div /div div classplaceholder placeholder-lg col-12/div div classplaceholder col-9 mt-2/div div classplaceholder col-11 mt-2/div /divplaceholder的原理很简单设置一个灰底背景配合placeholder-glow或placeholder-wave提供动画。col-6、col-9这些栅格类在这个场景里变成“占位块宽度比例”来用很方便模拟出不同长短的文字行rounded-circle配合固定宽高可以模拟头像。骨架屏最大的优势是降低焦虑感。用户看到一个和真实页面结构相似的轮廓大脑会自动预判“内容马上就出来”等待的煎熬感会大幅降低。相比全屏 spinner它也更符合内容型页面如新闻列表、商品卡片、后台表格的加载体验。我自己的实践原则是内容区域加载优先骨架屏全局操作等待优先 spinner。骨架屏适合“加载结果占据整个页面主体”的场景spinner 适合“某个动作执行中、完成后页面跳转或局部更新”的场景。两者并不冲突可以同时存在比如全局遮罩用 spinner遮罩下的内容区用骨架屏。4.3 多请求并发时 loading 的合并与释放时机页面初始化时经常要并行请求好几个接口比如用户信息、菜单权限、基础配置。初学者最容易犯的错是每个请求都独立控制一套 loading 逻辑结果出现“一个接口先回来loading 就消失了但页面数据还没完全就绪”的状态。正确的思路是合并 loading 状态。最简单的实现是维护一个计数器let pendingCount 0; function increaseLoading() { pendingCount; showPageLoading(); } function decreaseLoading() { pendingCount Math.max(0, pendingCount - 1); if (pendingCount 0) hidePageLoading(); } async function initPage() { increaseLoading(); try { const [userRes, menuRes, configRes] await Promise.allSettled([ fetch(/api/user), fetch(/api/menu), fetch(/api/config) ]); // 统一处理三个结果 } finally { decreaseLoading(); } }这段代码的关键在于Promise.allSettled它不会因为其中一个接口失败就立刻中断整体逻辑三个请求全部结束才会进入finally释放 loading。如果是Promise.all一个接口报错就抛异常虽然也会走到finally但另外两个接口的结果你就拿不到了。用计数器的好处是即使代码里多处调用了increaseLoading只要配对调用decreaseLoadingloading 就一定会在所有请求结束后才消失。这个模式在大型前端项目里几乎是标配。5. 进度条静态、条纹动画与 fetch 真实进度上报5.1 progress 组件的四个关键属性和堆叠多段进度有些场景只告诉用户“正在加载”还不够用户想知道“到哪一步了”。这种时候需要进度条。Bootstrap 5 的progress组件用法非常直接div classprogress roleprogressbar aria-label基本进度条 aria-valuenow60 aria-valuemin0 aria-valuemax100 div classprogress-bar stylewidth: 60%;/div /div外层容器progress是灰色轨道内部的progress-bar是高亮部分宽度通过内联样式控制。aria-valuenow、aria-valuemin、aria-valuemax三个属性是为了让读屏软件可以读出当前进度。需要注意aria-valuenow里写的是进度的数值而stylewidth写的是进度的百分比两者要保持同步这是一个很容易忽略的细节。Bootstrap 5 还给进度条提供了两个装饰类progress-bar-striped加上斜条纹progress-bar-animated让条纹滚动起来适合表示“正在处理中”的长任务比如导入导出数据。另外进度条可以堆叠多个progress-bar用不同颜色区分步骤或数据来源比如展示“已下载部分”和“已处理部分”。div classprogress roleprogressbar aria-label多段进度 aria-valuenow45 aria-valuemin0 aria-valuemax100 div classprogress-bar bg-success stylewidth: 30%;下载/div div classprogress-bar bg-warning stylewidth: 15%;校验/div /div堆叠时多个progress-bar共享同一个轨道宽度百分比相加不要超过 100否则会溢出。每个progress-bar都可以单独设置颜色和标签文字非常适合“下载解压导入”这类流程化操作。5.2 用 fetch 流式读取实现真实进度上报进度条最怕的是“假进度”也就是进度条卡在 50% 半天不动用户反而更焦虑。如果你用 fetch 请求后端文件或大数据接口其实可以实现真实的下载进度上报。原理是fetch返回的Response.body是一个ReadableStream我们可以通过response.body.getReader()逐块读取数据每读一块就累加字节数除以响应的总长度就能算出手动更新进度条。div classprogress mb-3 roleprogressbar aria-label下载进度 aria-valuenow0 aria-valuemin0 aria-valuemax100 div classprogress-bar iddownloadBar stylewidth: 0%;0%/div /divasync function downloadWithProgress(url) { const response await fetch(url); const contentLength Number(response.headers.get(Content-Length)); const reader response.body.getReader(); let received 0; let progressBar document.getElementById(downloadBar); while (true) { const { done, value } await reader.read(); if (done) break; received value.length; let percent Math.round((received / contentLength) * 100); progressBar.style.width percent %; progressBar.textContent percent %; progressBar.setAttribute(aria-valuenow, percent); } }需要说明的是这个方案有前提条件后端必须返回Content-Length响应头否则总长度未知没法算百分比。一些流式接口或 chunked 编码的接口可能不返回这个头这种情况下就要退回到“不确定进度”方案比如用progress-bar-striped progress-bar-animated做个无限滚动效果比卡在 50% 要好得多。另外reader.read()的循环里会持续占用 JavaScript 主线程如果每读一块就操作一次 DOM会有一定性能压力。我的经验是没有必要每块都更新进度条可以做一个节流比如每读取 10 块或每累计 1% 的进度才更新一次。否则在高速下载时进度条的 DOM 写入频率可能高到掉帧。5.3 进度数字显示与动画细节的补充处理进度条内部直接放文本的时候宽度很窄时文本会溢出或被截断Bootstrap 5 默认不会自动处理。如果你的进度条要显示百分比文字建议在进度条内部只有空间充足时才显示完整数字。最简单的做法是把百分比文字单独放在进度条上方或右侧而不是塞进进度条内部。div classd-flex justify-content-between mb-1 span正在导出数据.../span span idexportPercent0%/span /div div classprogress roleprogressbar aria-label导出进度 aria-valuenow0 aria-valuemin0 aria-valuemax100 div classprogress-bar idexportBar stylewidth: 0%;/div /div这样布局更稳定无论进度条当前宽度是多少数字都不会变形。JS 里更新时同时改#exportBar的宽度和#exportPercent的文本即可。进度条动画方面progress-bar默认是没有过渡动画的宽度从 10% 跳到 50% 是瞬时的。如果你希望进度条平滑地“爬”过去需要自己加一条 CSS.progress-bar { transition: width 0.3s ease; }注意这里有个反直觉的地方如果加过渡动画进度条在后退的时候也会平滑地变窄。有些场景比如重新请求、重置进度你希望进度条瞬间归零这时可以把过渡临时关掉操作完成后再打开。6. 我踩过的坑和无障碍细节动画丢失、闪烁与读屏支持6.1 三个高频坑display:none 重置动画、fixed 被 transform 击穿、spinner 的 border 参数第一个坑是display:none导致的动画丢失。如果你把spinner放进一个默认隐藏的容器里比如d-none然后在某个时机改成显示有概率出现 spinner 不转的情况。原因不难理解display:none会移除元素的渲染动画状态也被重置当它重新显示时需要重新触发动画而在某些浏览器里动画触发时机很尴尬。解决方法是不要用display:none控制 spinner 的显隐改用visibility配合opacity或者用d-flex之类不触发重排的属性切换。如果确实要用display切换那就在显示之后强制触发一次 reflow比如读取一下el.offsetWidth。第二个坑是position: fixed在transform父元素下失效。全屏遮罩最常见的实现是position: fixed但如果你在使用遮罩的同时页面上某个父容器加了transform动画比如弹窗的缩放过渡fixed 元素会变成相对该父容器定位遮罩就可能飘到奇怪的位置。这个坑尤其容易出现在“弹窗内部发起请求并显示全屏 loading”的场景。排查思路很简单打开开发者工具检查遮罩元素的定位上下文如果发现它的包含块不是视口就去找祖先元素上的transform或will-change删掉或调整结构即可。第三个坑是修改spinner-border的 border 参数。Bootstrap 5 的 border spinner 默认border-width是0.25em你可以调整这个值来改变圆环的粗细。但注意如果同时改了元素宽高和 border 宽度可能会出现圆环不是一个标准圆形的情况因为宽高是内容区宽度加上 border 之后整体会偏大。真要改尺寸优先用font-size因为宽高是基于em计算的font-size 变大整个圆环会等比放大不会变形。6.2 rolestatus 与 aria-label 怎么加才算对加载效果对读屏用户来说有两类信息需要传达一是“正在加载”这个状态二是“加载完成了”这个结果。Bootstrap 5 官方推荐在spinner内部放一个visually-hidden的文本同时在外层容器加rolestatus。div classspinner-border rolestatus span classvisually-hidden正在加载.../span /div有人会觉得rolestatus和aria-livepolite差不多其实在 ARIA 规范里rolestatus是aria-livepolite的语义化别名读屏软件会礼貌地播报它的内容变化。所以如果页面里有多个加载效果每个都应该有独立的、有意义的提示文字。还有一个经常被忽略的点按钮里嵌入 spinner 时如果按钮自己的文本已经从“保存”变成“保存中”就不需要在 spinner 里再写visually-hidden的“加载中”了加一个aria-hiddentrue让读屏直接跳过装饰性 spinner 即可避免同一个状态被播报两遍非常啰嗦。简单总结使用场景spinner 上怎么写为什么独立 loading 展示rolestatus 内部visually-hidden文本读屏用户需要知道“等待中”按钮/表单内嵌 loadingaria-hiddentrue按钮文字已表达状态避免重复播报全屏遮罩rolestatus 文本提示加载进度等待期较长需要更明确的反馈6.3 加载态闪烁的处理延迟出现与最小展示时长最后聊一个非常影响体验但常常没人注意的问题闪烁。如果你的接口平均只要 200 毫秒就返回但每次点击都闪一下加载效果用户会觉得系统“很卡、很跳”。这种体验问题出在加载效果的出现和消失都太快视觉上来不及完成一个平滑的过渡。解决方案有两个思路我通常在项目里配合使用。第一个是“延迟出现”。加载效果不立刻显示而是等一段时间后比如 300 毫秒还没完成才出现。这样短请求根本不会露出 loading长请求用户才会看到加载提示。let loadingTimer null; function showLoadingDelayed() { clearTimeout(loadingTimer); loadingTimer setTimeout(() { showPageLoading(); }, 300); } function hideLoading() { clearTimeout(loadingTimer); hidePageLoading(); }第二个是“最小展示时长”。如果 loading 已经出现了那它至少要在屏幕上停留一段时间比如 500 毫秒避免“刚出现 50 毫秒就消失”的闪烁。const startTime Date.now(); const MIN_LOADING_TIME 500; safeShowLoading(); try { const data await fetchData(); // 业务处理 } finally { const elapsed Date.now() - startTime; const restTime Math.max(0, MIN_LOADING_TIME - elapsed); setTimeout(hideLoading, restTime); }这个方案的核心是记录 loading 显示的时刻请求返回后计算已经过去的时间如果不足最小展示时长就用setTimeout补齐再隐藏。这两个策略配合起来加载效果会非常干净短请求无感知长请求稳定显示几乎不会有那种让人烦躁的闪动。我个人现在的项目里普遍会封装一个withLoading函数把延迟出现、最小展示时长、防重复提交全部收敛在一个工具函数里业务代码只负责调用加载状态的细节不用每个页面重复写。这个做法很推荐大家在自己的项目里沉淀一套类似的封装因为加载效果看着小散落在几十个业务代码里以后改一个交互细节就要动几十个地方成本非常高。加载效果是那种“做得好了没人注意做得差了人人抱怨”的细节。它的核心价值不是转圈本身而是维持用户对系统的信任。希望这篇文章里提到的原理、代码和坑能让你下次接到加载需求时少走几步弯路。
返回列表