js进度条怎么用:从准备到完成的实操步骤

AI教程9小时前发布 jdkjadmin
0 00
js进度条怎么用:从准备到完成的实操步骤

js进度条怎么用:从准备到完成的实操步骤

JavaScript 进度条的核心工作,是把任务的完成情况转成用户看得懂的反馈。实现时不只是修改一个百分比:还要确认任务是否真的能计算进度、正确处理开始与结束状态,并让使用辅助技术的用户也能理解进展。本文从原生元素开始,逐步实现模拟任务和文件上传进度,并说明常见边界与安全注意事项。

一、先判断任务是否适合显示进度

进度条适用于能够衡量“已完成量”和“总量”的任务,例如文件上传或下载、已知数量的批量处理、分阶段导入,以及有明确步骤的操作。如果任务无法获得可靠的总量,就不能准确计算百分比。此时应使用不确定状态,而不是编造一个看似精确的数字。

可以先问自己三个问题:

  • 总量是否已知?知道文件总字节数或任务总步数,通常可以显示确定进度。
  • 完成量是否可观测?进度应来自实际已处理的数据或步骤,而不是单纯依靠计时器猜测。
  • 任务何时算完成?请求发送完毕不一定代表服务器已经处理成功。应以实际业务结果决定是否显示 100%。

如果只是显示页面正在加载,但无法得知总量,可以使用旋转指示器或不确定进度条。不要让一个进度条永远停在 90%,也不要在任务尚未完成时先显示 100%。

二、准备页面:优先使用原生 progress

HTML 的 <progress> 元素适合表示任务进度。设置 max 为总量,设置 value 为当前完成量,再为进度条提供可访问名称。

<label for="task-progress">处理进度</label>
<progress id="task-progress" max="100" value="0"></progress>
<span id="task-percent">0%</span>

max 必须是大于 0 的有限值;如果省略,默认值是 1。value 表示当前值,应该在 0 到 max 之间。省略 value 时,进度条进入不确定状态,适合总量未知的任务。实际开发中建议显式设置合理的 max,并在 JavaScript 中确保传入的数值有效,避免依赖浏览器对异常输入的处理。

这里用 <label> 关联进度条,让屏幕阅读器用户知道它代表什么。单独显示一个百分比文本不能替代这个名称;反过来,进度条本身也不一定会自动显示百分比数字。

三、用 JavaScript 更新确定进度

下面是一个可直接放入 HTML 页面的示例。点击按钮后模拟一个有 20 个步骤的任务。示例中的计时器仅用于演示界面更新,真实业务中应将进度绑定到任务实际状态。

<button id="start-button" type="button">开始任务</button>
<label for="task-progress">处理进度</label>
<progress id="task-progress" max="20" value="0"></progress>
<span id="task-percent">0%</span>
<p id="task-status" aria-live="polite">尚未开始</p>

<script>
  const progress = document.querySelector("#task-progress");
  const percentText = document.querySelector("#task-percent");
  const statusText = document.querySelector("#task-status");
  const startButton = document.querySelector("#start-button");

  let timer = null;

  function renderProgress(current, total) {
    // total 必须大于 0,current 应限制在 0 到 total 之间。
    const safeTotal = Math.max(1, Number(total) || 1);
    const safeCurrent = Math.min(
      safeTotal,
      Math.max(0, Number(current) || 0)
    );

    progress.max = safeTotal;
    progress.value = safeCurrent;

    const percent = Math.round((safeCurrent / safeTotal) * 100);
    percentText.textContent = percent + "%";
  }

  startButton.addEventListener("click", () => {
    if (timer !== null) {
      return;
    }

    let completed = 0;
    const total = 20;

    renderProgress(0, total);
    statusText.textContent = "正在处理…";
    startButton.disabled = true;

    timer = window.setInterval(() => {
      completed += 1;
      renderProgress(completed, total);

      if (completed >= total) {
        window.clearInterval(timer);
        timer = null;
        statusText.textContent = "处理完成";
        startButton.disabled = false;
      }
    }, 150);
  });
</script>

progress.value 和 progress.max 都是数值属性。页面中的百分比文本由代码单独更新,是为了让数字对用户可见。示例还设置了 aria-live="polite",让状态变化能以较温和的方式通知辅助技术用户。对于高频变化,不要每一帧都播报长文本;可以只在开始、完成或关键阶段更新状态消息。

四、用阶段进度表达没有精确百分比的任务

有些任务能确定步骤,却不能准确衡量每一步的耗时。例如“读取文件、校验数据、提交结果”三步。此时可以按已完成步骤计算一个阶段比例,但要说明它表示步骤进展,不代表真实耗时比例。

const steps = ["读取文件", "校验数据", "提交结果"];
let finishedSteps = 0;

function updateStepProgress() {
  const total = steps.length;
  progress.max = total;
  progress.value = finishedSteps;

  const percent = Math.round((finishedSteps / total) * 100);
  percentText.textContent = percent + "%";

  statusText.textContent =
    finishedSteps < total
      ? "当前阶段:" + steps[finishedSteps]
      : "全部步骤完成";
}

// 某一步实际完成后调用:
// finishedSteps += 1;
// updateStepProgress();

不要因为每个阶段耗时不同,就把“完成两步”直接描述成“已经完成三分之二的工作量”。这种比例只是步骤占比。若阶段耗时差异很大,更合适的方式是显示当前阶段和状态,而不是制造精确感。

五、文件上传:读取真实的传输进度

如果需要跟踪浏览器向服务器上传文件的字节进度,可以使用 XMLHttpRequest 的上传进度事件。事件中的 loaded 是已传输字节数,total 是可计算时的总字节数。只有在 lengthComputable 为 true 时,才能据此计算百分比。

下面的示例需要一个实际可接收文件的服务端接口。示例使用 /upload 作为接口地址,它不是浏览器内置的上传服务;部署时应替换为自己的接口,并由服务端完成认证、校验和存储。

<input id="file-input" type="file">
<button id="upload-button" type="button">上传</button>
<label for="upload-progress">上传进度</label>
<progress id="upload-progress"></progress>
<span id="upload-percent"></span>
<p id="upload-status" aria-live="polite">请选择文件</p>

<script>
  const fileInput = document.querySelector("#file-input");
  const uploadButton = document.querySelector("#upload-button");
  const uploadProgress = document.querySelector("#upload-progress");
  const uploadPercent = document.querySelector("#upload-percent");
  const uploadStatus = document.querySelector("#upload-status");

  uploadButton.addEventListener("click", () => {
    const file = fileInput.files[0];

    if (!file) {
      uploadStatus.textContent = "请先选择文件";
      return;
    }

    const formData = new FormData();
    formData.append("file", file);

    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/upload");

    uploadProgress.removeAttribute("value");
    uploadProgress.removeAttribute("max");
    uploadPercent.textContent = "";
    uploadStatus.textContent = "正在准备上传…";
    uploadButton.disabled = true;

    xhr.upload.addEventListener("progress", (event) => {
      if (!event.lengthComputable || event.total <= 0) {
        uploadProgress.removeAttribute("value");
        uploadStatus.textContent = "正在上传,暂时无法计算进度";
        return;
      }

      uploadProgress.max = event.total;
      uploadProgress.value = event.loaded;

      const percent = Math.round(
        (event.loaded / event.total) * 100
      );
      uploadPercent.textContent = percent + "%";
      uploadStatus.textContent = "正在上传";
    });

    xhr.addEventListener("load", () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        uploadProgress.max = 1;
        uploadProgress.value = 1;
        uploadPercent.textContent = "100%";
        uploadStatus.textContent = "上传请求成功";
      } else {
        uploadStatus.textContent =
          "服务器返回错误,状态码:" + xhr.status;
      }
    });

    xhr.addEventListener("error", () => {
      uploadStatus.textContent = "网络错误,上传失败";
    });

    xhr.addEventListener("abort", () => {
      uploadStatus.textContent = "上传已取消";
    });

    xhr.addEventListener("loadend", () => {
      uploadButton.disabled = false;
    });

    xhr.send(formData);
  });
</script>

这个例子在进度不可计算时移除 value,让进度条显示不确定状态;能计算时才设置 max 和 value。文件传输进度达到 100% 只说明浏览器已经发送完请求体,不必然代表服务端已成功保存或处理文件。因此示例只有在收到成功的 HTTP 状态码后,才把状态更新为“上传请求成功”。实际接口还可能需要检查响应内容中的业务结果。

文件上传接口应使用 HTTPS,并由服务端进行授权、文件大小限制、文件类型检查和必要的内容安全扫描。浏览器提供的文件名、扩展名、MIME 类型都不能单独作为安全依据。不要把认证凭据或敏感信息写进公开代码,也不要把进度条当成权限控制或文件校验机制。

六、下载和 Fetch 的适用边界

下载进度通常要在响应流可读取、且总大小已知时才能计算。常见做法是读取响应头中的 Content-Length,再通过流逐块累计字节数。不过这个响应头可能不存在,也可能因为压缩、代理或传输方式而不能直接作为可用总量;实际应以可获得的数据为准。若无法确认总量,就显示不确定状态。

Fetch 的请求体上传进度在浏览器中的支持和使用条件与响应体下载不同,不应假设所有环境都能通过普通的 fetch 调用监听上传字节进度。若业务必须提供稳定的上传进度,应针对目标浏览器验证方案;XMLHttpRequest 的 upload 进度事件仍是常见选择。还要注意跨域请求:跨域设置、预检请求和服务端响应头都可能影响请求能否成功,不能仅凭前端进度事件判断跨域上传配置正确。

七、什么时候需要自定义进度条

原生 <progress> 通常是更简单、更稳妥的选择。如果设计要求无法由浏览器默认外观满足,可以用普通元素自定义视觉效果,但需要自己负责无障碍语义和状态更新。

<div
  class="progress-track"
  role="progressbar"
  aria-label="处理进度"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuenow="0"
>
  <div class="progress-fill"></div>
</div>

<style>
  .progress-track {
    width: 100%;
    height: 12px;
    overflow: hidden;
    border-radius: 6px;
    background: #e5e7eb;
  }

  .progress-fill {
    width: 0%;
    height: 100%;
    background: #2563eb;
    transition: width 160ms ease;
  }

  @media (prefers-reduced-motion: reduce) {
    .progress-fill {
      transition: none;
    }
  }
</style>

<script>
  const track = document.querySelector(".progress-track");
  const fill = document.querySelector(".progress-fill");

  function setCustomProgress(value, max) {
    const safeMax = Number(max);
    const safeValue = Number(value);

    if (!Number.isFinite(safeMax) || safeMax <= 0) {
      throw new Error("max 必须是大于 0 的有限数值");
    }
    if (!Number.isFinite(safeValue)) {
      throw new Error("value 必须是有限数值");
    }

    const clamped = Math.min(safeMax, Math.max(0, safeValue));
    const percent = (clamped / safeMax) * 100;

    track.setAttribute("aria-valuemin", "0");
    track.setAttribute("aria-valuemax", String(safeMax));
    track.setAttribute("aria-valuenow", String(clamped));
    fill.style.width = percent + "%";
  }

  setCustomProgress(35, 100);
</script>

自定义确定进度条要提供 role="progressbar"、可访问名称,以及最小值、最大值、当前值等状态。总量未知时,不应设置 aria-valuenow;确定状态和不确定状态的语义不能混用。进度条内部的文本内容也不一定会被辅助技术当作其名称,因此名称应通过 aria-label 或关联标签提供。

对于原生元素和自定义元素,都应避免只靠颜色区分状态。必要时同时显示百分比、阶段名称或状态文字,并确保文本与背景有足够对比度。动画宜简洁,也要尊重用户的减少动态效果设置。

八、常见错误与修正方法

  1. 把未知进度伪装成确定百分比。总量未知时,使用不确定状态或阶段提示;不要让数字随着时间增长,却与实际工作无关。
  2. 只更新宽度,不更新真实状态。自定义进度条需要同步更新 ARIA 数值和可见文本,否则视觉进展与辅助技术读取的信息可能不一致。
  3. 忘记处理 0、负数、无穷大和非数字。计算百分比前先验证输入,并确保总量大于 0。文件字节数或任务计数应采用实际可信的数据。
  4. 把请求发出当作任务完成。网络请求可能失败,服务端也可能拒绝或处理失败。根据响应状态和业务结果更新完成状态。
  5. 每次都创建新计时器。重复点击可能启动多个定时器。需要禁用按钮、保存定时器引用,并在完成或取消时清理。
  6. 过度频繁更新页面。不必为了追求平滑而高频操作 DOM。按实际事件更新即可;若任务事件特别密集,可适度合并视觉更新,但不能改变用户理解的实际状态。
  7. 将百分比直接拼进 HTML。文本应优先通过 textContent 更新。若把外部数据插入 innerHTML,可能引入跨站脚本风险。
  8. 把前端校验当作安全防护。进度条只负责反馈。权限检查、文件校验、速率限制和最终处理结果必须由服务端按实际需求实施。

九、实施前检查清单

  • 任务有可靠的总量吗?没有时是否使用不确定状态?
  • 进度数值是否经过校验,并限制在有效范围内?
  • 是否显示了进度条的名称、当前状态和必要的百分比文本?
  • 是否处理成功、失败、取消、网络错误和重复操作?
  • 对于上传,是否区分“传输完成”和“服务端处理成功”?
  • 是否避免将不可信内容作为 HTML 插入页面,并把安全校验放在正确的服务端边界?

实现 JavaScript 进度条时,先选对反馈方式,再把真实任务状态映射到原生 <progress> 或自定义组件。只要明确总量是否已知、正确处理异常状态,并兼顾无障碍和服务端结果,进度条就能提供有用反馈,而不只是页面上的一条装饰性动画。

© 版权声明

相关文章