JS 进度条怎么写?scaleX 动画、真实上传与后台任务上报
js 进度条的实现分四类:CSS 静态条、JS 逐帧更新、真实进度(下载/上传)、以及顶部加载条(nprogress 式)。多数人写出的版本“能动但卡顿”“读不出真实百分比”,原因在动画手段选错和没有可量化的进度源。本文给出四种可用写法。

一、纯 CSS 不确定进度(“转圈式”进度条)
只知道“在处理中”,不知道百分比时,用 CSS 动画最省:
<div class="bar indeterminate"><div class="fill"></div></div>
.bar { height:4px; background:#e6e8ef; overflow:hidden; position:relative; border-radius:2px }
.bar .fill { position:absolute; left:-40%; width:40%; height:100%;
background:linear-gradient(90deg,#3b82f6,#60a5fa); }
.indeterminate .fill { animation:slide 1.1s infinite ease-in-out }
@keyframes slide {
0% { left:-40% }
100% { left:100% }
}
注意动画走的是 left,其实更好的做法是 transform: translateX()——GPU 合成、不触发布局。生产建议改成 transform。
二、确定进度:用 transform 平滑更新
<div class="bar"><i style="transform:scaleX(.3)"></i></div>
.bar > i { display:block; height:100%; width:100%; transform-origin:left;
background:#3b82f6; transition:transform .25s ease }
// JS:直接改 scaleX
function setProgress(p) { // p: 0~1
el.style.transform = 'scaleX(' + Math.max(0, Math.min(1, p)) + ')';
}
scaleX 比改 width 快得多(避免 layout)。配合 transition 天然平滑,不需要自己写缓动函数。
三、真实进度:必须有可量化的数据源
1. 文件上传(XHR 有 progress 事件,fetch 没有)
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = e => {
if (e.lengthComputable) setProgress(e.loaded / e.total);
};
xhr.onload = () => setProgress(1);
xhr.onerror = () => { /* 失败状态,不要停在半截 */ };
const fd = new FormData();
fd.append('file', input.files[0]);
xhr.send(fd); // 进度是真实字节比例
fetch 想用进度要走 ReadableStream + Content-Length,或用 axios 的 onUploadProgress/onDownloadProgress(底层仍是 XHR)。
2. 后端任务进度(导入、生成报表、转码)
这类没有字节概念,必须服务端上报。三种轮子,按可靠性排序:
- Redis 计数器 + 前端轮询:任务每处理一批
INCR done:{jobId},前端每 1~2 秒查done/total。实现最简单,容错最好; - SSE(Server-Sent Events):比 WebSocket 轻,服务端
text/event-stream推事件,浏览器new EventSource('/progress?id=123')直接收,注意要关闭代理缓冲(Nginxproxy_buffering off;); - WebSocket:双向、适合高频,但要多维护连接状态与重连逻辑。
// 前端:SSE 版
const es = new EventSource('/job/progress?id=' + jobId);
es.onmessage = ev => {
const { done, total, state } = JSON.parse(ev.data);
setProgress(total ? done / total : 0);
if (state === 'finished' || state === 'failed') es.close();
};
es.onerror = () => es.close(); // 必须处理,否则会无限重连
3. 前端自己算:批量任务分片
async function processAll(list, onStep) {
for (let i = 0; i < list.length; i++) {
await doOne(list[i]);
onStep((i + 1) / list.length);
if (i % 20 === 19) await new Promise(r => setTimeout(r)); // 让出主线程,避免界面卡死
}
}
那个 setTimeout 让帧很重要:不让出主线程,进度条就“走完最后一步才刷新”,看起来像卡住。
四、顶部加载条(页面跳转反馈)
nprogress 式“页面顶部的细条”其实是假进度(前 80% 快、后 20% 慢),目的是缩短感知等待。要点写法:
let t = 0, timer;
function start() {
setProgress(0);
timer = setInterval(() => {
t = Math.min(0.92, t + (0.06 - t) * 0.2); // 越接近完成涨得越慢
setProgress(t);
}, 120);
}
function done() { clearInterval(timer); setProgress(1); setTimeout(() => hide(), 300); }
注意 done() 后要有短暂停留再隐藏,否则闪一下就没了。
五、无障碍与体验细节
- 进度条加 ARIA:
role="progressbar" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100" aria-label="上传进度",让读屏用户能获取状态; - 百分比要同时用文字显示(纯视觉进度对色觉障碍不友好);
- 失败态要能重试,且不要把进度重置为 0(保留位置更符合心智);
- 耗时任务提供“取消”,前端调
xhr.abort(),服务端要有对应的中断处理; - 不要用
setInterval(100)固定步长模拟真实上传——一旦实际网络波动,进度会严重失真。
六、常见问题 FAQ
问:为什么进度到了 99% 就停住?
答:多数是“等待服务端最终返回”的阶段没算进进度。做法:把 onload 前设为 0.9,收到响应再置 1,并在 UI 上改成“处理中…”。
问:<progress> 标签能用吗?
答:能用(HTML 原生,<progress value="30" max="100">),但样式跨浏览器差异大,需要伪元素定制,工业项目仍多用自定义 div。
问:进度条动画掉帧?
答:检查是否在改 width/top/left(触发 layout),改 transform 后一般就顺了;同时避免同一帧里大量读写 DOM。
选型规则:不知道百分比 → CSS 动画;知道 → scaleX;上传下载 → XHR progress;后台任务 → Redis+轮询或 SSE;页面跳转反馈 → 假进度但别太久。
生产内容,同时打通搜索引擎(SEO)与AI搜索引擎(GEO)流量入口的软件! 点击进入365建站器 »
