
js进度条怎么用:从准备到完成的实操步骤
进度条的工作看起来简单:把一个数字转换成一段填充宽度。但实际开发时,难点通常不在动画,而在于这个数字是否可信、什么时候更新、任务结束后如何处理,以及辅助技术和用户能不能正确理解它。
本文从一个可以直接运行的原生 HTML、CSS 和 JavaScript 示例开始,逐步说明确定进度、未知进度、异步任务接入、无障碍和安全边界。示例不依赖框架,保存为 HTML 文件后即可在浏览器中运行。
一、先判断:这个任务适合用进度条吗
进度条适合表示一个正在进行、用户需要等待,而且能够说明其进展或忙碌状态的任务。常见场景包括文件上传与下载、批量处理、导入导出、分步初始化、后台计算,以及多个子任务组成的操作。
如果能够取得已完成量和总量,可以展示确定进度,例如“已上传 30 MB,共 100 MB”。如果无法合理估算剩余工作,就应展示不确定进度,例如一个持续滚动的忙碌指示器,而不是伪造一个从 0% 缓慢增长的数值。
短到几乎不需要等待的操作,通常不必显示进度条。对于“页面正在提交”这类无法计算进度、但确实需要提示等待的操作,可以显示不确定状态,并在结束时及时更新提示。若任务需要用户知道具体阶段,进度条旁还应提供文字,如“正在读取文件”或“正在写入数据”;百分比不能代替任务说明。
二、准备工作:选用原生还是自定义进度条
优先考虑 HTML 原生的 <progress> 元素。它提供了进度的语义,辅助技术更容易识别,也减少了手写 ARIA 属性时出错的机会。它适合常见的整体任务进度,外观可通过 CSS 调整,但不同浏览器和操作系统对其内部绘制的支持与表现可能不同。
只有在需要特殊视觉效果、分段展示或更细致控制时,才考虑自定义进度条。自定义方案必须自行实现进度语义、键盘与辅助技术体验,以及数值同步。不要因为能画出彩色条就认为它已经是可访问的进度条。
还需要先确认数据来源:任务是否能给出总量,完成量是否单调递增,是否可能取消、失败或重试。若总量未知,或者服务端会动态改变总量,就不应把未经确认的估值包装成精确百分比。
三、最小可运行示例:原生 progress 元素
将下面内容保存为 progress-demo.html,用浏览器打开。点击按钮后,进度会每隔一段时间更新;也可以随时取消。示例中的计数仅用于演示,不代表真实工作的完成情况。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>进度条示例</title>
<style>
body {
max-width: 42rem;
margin: 3rem auto;
padding: 0 1rem;
font: 16px/1.6 system-ui, sans-serif;
}
progress {
display: block;
width: 100%;
height: 1.25rem;
margin: 1rem 0;
accent-color: #1769aa;
}
.status {
min-height: 1.6em;
}
button {
margin-right: .5rem;
padding: .45rem .8rem;
}
</style>
</head>
<body>
<h1>处理任务</h1>
<label for="task-progress">任务进度</label>
<progress id="task-progress" max="100" value="0">0%</progress>
<p id="status" class="status" role="status" aria-live="polite">
尚未开始
</p>
<button id="start" type="button">开始</button>
<button id="cancel" type="button" disabled>取消</button>
<script>
const progress = document.querySelector("#task-progress");
const status = document.querySelector("#status");
const startButton = document.querySelector("#start");
const cancelButton = document.querySelector("#cancel");
let timer = null;
let current = 0;
const total = 100;
function finish(message) {
clearInterval(timer);
timer = null;
startButton.disabled = false;
cancelButton.disabled = true;
status.textContent = message;
}
startButton.addEventListener("click", () => {
if (timer !== null) return;
current = 0;
progress.value = current;
status.textContent = "正在处理…";
startButton.disabled = true;
cancelButton.disabled = false;
timer = setInterval(() => {
current = Math.min(current + 5, total);
progress.value = current;
progress.textContent = current + "%";
if (current === total) {
finish("处理完成");
}
}, 200);
});
cancelButton.addEventListener("click", () => {
finish("已取消");
});
</script>
</body>
</html>
这里的 max="100" 表示最大值为 100,脚本将 value 设置为当前完成值。原生 <progress> 的 max 默认值是 1;如果指定 max,它必须是大于 0 的有限数值。没有设置 value 时,元素表示不确定进度;设置了 value 则表示确定进度。应用代码应保证完成值处于 0 到最大值之间,不要依赖浏览器对异常输入的处理来维护业务正确性。
上例的 <label> 为进度条提供可读名称。<progress> 内部的文字是浏览器不支持该元素时的回退内容,不应把它当成唯一的可访问名称。旁边的状态文字用于说明任务发生了什么;role="status" 和 aria-live="polite" 让辅助技术有机会在合适时机播报状态变化。
四、实际接入时,进度值从哪里来
示例用定时器递增,只适合演示界面更新。真实项目应使用任务本身提供的进度数据,而不是用固定时间猜测完成比例。常见计算方式是:
const percent = total > 0
? Math.floor((completed / total) * 100)
: null;
需要先处理总量为零、缺失、无穷大或无效数值的情况。下面是一个较稳妥的更新函数,要求调用方传入有效的完成量和总量;不满足条件时不显示虚假的确定百分比。
function updateProgress(progressElement, completed, total) {
const valid =
Number.isFinite(completed) &&
Number.isFinite(total) &&
total > 0;
if (!valid) {
// 删除 value 后,原生 progress 表示不确定进度。
progressElement.removeAttribute("value");
return null;
}
const bounded = Math.min(Math.max(completed, 0), total);
progressElement.max = total;
progressElement.value = bounded;
return Math.floor((bounded / total) * 100);
}
这个函数把完成量限制在 0 到总量之间,并返回供界面文字使用的整数百分比。是否需要把任务失败、取消或重试显示为 100%,应由业务状态决定:失败和取消不是完成,通常不应通过把进度条填满来暗示成功。任务成功后可以保留满格进度并显示“完成”,也可以在短暂展示结果后隐藏进度条。
如果总量会动态变化,百分比可能倒退。例如处理队列中途又加入了新项目,总任务量增大,完成比例自然下降。此时应明确告知用户任务范围发生变化,或展示已处理数量与当前总量,而不是强行让百分比单调上升。
五、异步任务:用真实事件更新,而不是延时模拟
文件上传进度
浏览器中的 XMLHttpRequest 可以通过 xhr.upload 的 progress 事件报告上传进展。下面的函数展示了接入方式;实际使用时应把文件放进表单数据,并提供真实服务端地址。
function uploadFile(file, url, progressElement, statusElement) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file);
xhr.open("POST", url);
xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable && event.total > 0) {
const percent = Math.floor(
(event.loaded / event.total) * 100
);
progressElement.max = 100;
progressElement.value = percent;
statusElement.textContent = `正在上传:${percent}%`;
} else {
progressElement.removeAttribute("value");
statusElement.textContent = "正在上传,暂时无法计算进度";
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
progressElement.value = 100;
statusElement.textContent = "上传完成";
} else {
statusElement.textContent = `上传失败,HTTP ${xhr.status}`;
}
});
xhr.addEventListener("error", () => {
statusElement.textContent = "网络错误,上传未完成";
});
xhr.addEventListener("abort", () => {
statusElement.textContent = "上传已取消";
});
xhr.send(formData);
return xhr;
}
调用该函数后可以保存返回的 xhr,需要取消时调用 xhr.abort()。只有 event.lengthComputable 为真且总量大于零时,才能据此计算比例;否则应显示不确定状态。上传事件所报告的数据量不一定等同于服务端最终保存成功的数据,也不代表服务器端后续处理已经完成。若上传后还要转码或入库,应将“上传完成”和“服务端处理完成”作为不同阶段呈现。
下载与 Fetch 的边界
Fetch API 的常规请求接口没有一个可以直接监听的通用下载进度事件。对于可读取响应流的场景,可以通过响应体流读取已接收的数据,再与已知的响应长度比较;但响应头中的长度可能缺失,也可能受压缩、代理或服务端配置影响,不能据此在所有环境里承诺准确百分比。跨域请求还受到 CORS 规则限制,某些响应头需要服务端明确允许浏览器读取。
因此,下载进度只有在已知且可信的总长度可用时才展示确定百分比。否则使用不确定状态,并说明“正在下载”即可。也不要将请求已经收到响应头误认为文件已经下载完成;只有数据流读取结束后,下载阶段才算结束。
六、不确定进度:不要制造虚假的百分比
原生 <progress> 不设置 value 就表示进度未知。可以配合文字说明任务正在运行:
<label for="waiting-progress">数据处理</label>
<progress id="waiting-progress" max="100">
正在处理
</progress>
<p role="status" aria-live="polite">正在处理,请稍候。</p>
这里虽然设置了最大值,但没有设置当前值,所以不是 0%。当任务结束或失败时,应更新状态并移除或隐藏忙碌指示器。具体的不确定动画外观由浏览器决定;如果产品需要自定义动画,要考虑减少动态效果的系统设置,并确保文字状态仍然可用。
七、自定义进度条:补齐语义与范围校验
如果确实需要完全控制外观,可以使用普通元素绘制,但要用 role="progressbar"、名称以及当前值、最小值和最大值表达进度。下面的函数校验输入,并同时更新可视宽度和 ARIA 属性。自定义进度条本身不是可交互控件,通常不需要加入 Tab 键顺序。
<div class="custom-progress"
role="progressbar"
aria-label="文件处理进度"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0">
<div class="custom-progress__fill"></div>
</div>
<style>
.custom-progress {
height: 1rem;
overflow: hidden;
border-radius: .5rem;
background: #e5e7eb;
}
.custom-progress__fill {
width: 0%;
height: 100%;
background: #1769aa;
transition: width 150ms linear;
}
@media (prefers-reduced-motion: reduce) {
.custom-progress__fill {
transition: none;
}
}
</style>
<script>
function setCustomProgress(element, value, max = 100) {
if (!Number.isFinite(value) ||
!Number.isFinite(max) ||
max <= 0) {
throw new TypeError("value 和 max 必须是有效数值,max 必须大于 0");
}
const current = Math.min(Math.max(value, 0), max);
const percent = (current / max) * 100;
const fill = element.querySelector(".custom-progress__fill");
element.setAttribute("aria-valuemin", "0");
element.setAttribute("aria-valuemax", String(max));
element.setAttribute("aria-valuenow", String(current));
fill.style.width = percent + "%";
}
const bar = document.querySelector(".custom-progress");
setCustomProgress(bar, 35, 100);
</script>
自定义不确定状态时,不应保留一个看似精确的 aria-valuenow。可以移除该属性,并在任务结束后同步清理动画和状态。如果页面中有多个进度条,每个都要有能够区分任务的名称。只设置颜色或宽度而不更新 ARIA 数值,会造成屏幕阅读器读到过期信息;只更新 ARIA 而不更新视觉填充,也会造成不同用户看到的状态不一致。
八、无障碍与界面反馈检查
- 有名称:用关联的
<label>,或在适当情况下用aria-label标明进度对应的任务。 - 数值同步:视觉显示、原生元素的
value或自定义元素的 ARIA 数值应来自同一份状态。 - 状态明确:失败、取消、完成和仍在处理中是不同状态,不要只靠颜色区分。
- 避免频繁播报:如果每几十毫秒更新一次百分比,屏幕阅读器可能被大量消息打扰。可让进度值正常更新,但将文字播报限制在阶段变化或间隔较长的节点。
- 考虑减少动态效果:自定义动画可响应
prefers-reduced-motion,并避免持续闪烁。
进度条是状态展示,不是等待时间承诺。除非能够基于可靠数据估算剩余时长,否则不要把一段动画解释为“还剩几秒”。对关键任务,最好同时提供取消操作或其他可行的退出方式,并在取消请求真正生效后再显示取消结果。
九、常见错误与修正方式
1. 用定时器假装真实进度
每秒加 10%,看起来直观,但任务可能提前完成、耗时更久或中途失败。定时器适合演示或没有确定进度时的忙碌动画,不适合冒充实际完成比例。优先使用上传事件、处理队列状态或服务端任务状态。
2. 总量为零仍计算百分比
用 completed / total 计算时,total 为零会产生无效结果。应先判断总量是否是正的有限数;无法计算时切换为不确定状态,不要把 NaN、Infinity 或异常百分比写入界面。
3. 只改宽度,不更新语义
自定义元素若只修改 CSS 宽度,辅助技术可能不知道进度发生了变化。应同步更新 aria-valuenow、范围和任务名称。能使用原生 <progress> 时,通常更省心。
4. 把 100% 当作任务成功
传输进度达到 100% 不一定表示服务端校验、保存或后续处理已成功。应根据服务端响应决定是否显示完成;失败时显示明确错误,并允许用户按产品设计重试。
5. 进度倒退时强行隐藏问题
分母变化、重试或服务端重新估算都可能导致比例下降。直接取历史最大值会让显示与真实状态脱节。应解释任务范围变化,或改为展示阶段和已处理数量。
6. 高密度更新状态文字
把每一次底层进度事件都写入实时播报区域,可能让辅助技术持续朗读。可降低文字更新频率,例如每跨过 5% 或 10% 才更新一次播报文字,同时保留界面进度值的正常刷新。
十、安全边界:进度条不负责授权,也不验证任务结果
进度条只负责呈现客户端掌握的状态,不能作为安全控制。客户端显示 100% 不代表文件可信、请求已获授权或服务端操作成功;服务端仍必须独立执行身份验证、权限检查、输入校验和必要的文件安全检查。用户也可以通过开发者工具修改页面上的进度数值,不能用它决定是否允许敏感操作。
把任务名、服务端消息或文件名显示到页面时,应作为文本写入。例如使用 textContent,不要把未经信任的字符串拼接进 innerHTML,以免把内容当作 HTML 解析并造成跨站脚本风险。进度条的数值也应进行有限数值检查和范围限制,避免异常输入破坏界面或导致错误的业务判断。
十一、上线前的实操检查清单
- 确认任务是可计算进度,还是只能表示忙碌状态;未知时不编造百分比。
- 优先用原生
<progress>,并为它提供可读名称。 - 让进度来源于真实事件或任务状态;演示定时器不要混入生产逻辑。
- 校验完成量和总量,处理总量缺失、为零、无效或变化的情况。
- 分别处理成功、失败、取消和重试,不把它们统统显示为 100%。
- 检查键盘、屏幕阅读器、窄屏布局和减少动态效果设置下的表现。
- 所有外部文本按纯文本呈现;授权与任务成功由服务端和业务状态判断,而非由进度条判断。
实现一个可靠的 JavaScript 进度条,核心不是填满的速度,而是如实表达任务状态。先选对原生或自定义方案,再让数据、视觉表现和辅助技术语义保持一致;对于无法测量的工作,清楚说明“正在处理”往往比一个看似精确、实际不可信的百分比更有帮助。




