Back

如何使用 JavaScript 构建上传进度条

如何使用 JavaScript 构建上传进度条

上传文件时如果没有视觉反馈,会让用户不知道是否有任何进展。进度条可以将这种不确定性转化为清晰、易用的体验,准确显示上传完成的进度。

本文演示如何使用 JavaScript 的 XMLHttpRequest API、语义化 HTML 元素和无障碍最佳实践来构建实时上传进度条——创建一个在所有现代浏览器中都能可靠工作的解决方案。

核心要点

  • XMLHttpRequest 仍然是跟踪上传进度的标准 API,因为 Fetch API 不支持上传进度事件
  • 带有 ARIA 属性的语义化 HTML 确保所有用户的可访问性
  • 该解决方案无需外部依赖即可在所有现代浏览器中工作
  • 适当的错误处理和用户控制创建了强大的上传体验

设置 HTML 结构

从提供视觉和可访问性反馈的语义化 HTML 开始:

<form id="uploadForm">
  <label for="fileInput">Select file to upload:</label>
  <input type="file" id="fileInput" name="file" accept="image/*,application/pdf">
  
  <progress id="uploadProgress" value="0" max="100" aria-label="Upload progress"></progress>
  <span id="progressText" aria-live="polite">0% uploaded</span>
  
  <button type="submit">Upload File</button>
  <button type="button" id="cancelBtn" disabled>Cancel Upload</button>
</form>

<progress> 元素提供了辅助技术能够理解的原生语义。附带的文本百分比确保用户不仅仅依赖视觉提示。aria-live="polite" 属性会向屏幕阅读器播报百分比变化,而不会中断其他内容。

为什么使用 XMLHttpRequest 来实现上传进度

虽然 Fetch API 能够优雅地处理大多数现代 HTTP 请求,但它仍然不支持上传进度事件。XMLHttpRequest 对象仍然是跟踪上传进度实现的正确工具,因为它提供了 xhr.upload.onprogress 事件处理程序。

请注意,只有同步 XMLHttpRequest 被弃用——异步 XHR 仍然是一个基础的、得到良好支持的 API,非常适合这个用例。

使用 JavaScript 实现上传进度条

以下是处理文件选择、上传跟踪和用户控制的完整实现:

const form = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('uploadProgress');
const progressText = document.getElementById('progressText');
const cancelBtn = document.getElementById('cancelBtn');

let currentXHR = null;

form.addEventListener('submit', (e) => {
  e.preventDefault();
  
  const file = fileInput.files[0];
  if (!file) return;
  
  // Validate file size (10MB limit example)
  const maxSize = 10 * 1024 * 1024;
  if (file.size > maxSize) {
    alert('File size exceeds 10MB limit');
    return;
  }
  
  uploadFile(file);
});

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  currentXHR = new XMLHttpRequest();
  
  // Track upload progress
  currentXHR.upload.onprogress = (event) => {
    if (event.lengthComputable) {
      const percentComplete = Math.round((event.loaded / event.total) * 100);
      updateProgress(percentComplete);
    } else {
      // Handle indeterminate progress
      progressBar.removeAttribute('value');
      progressText.textContent = 'Uploading...';
    }
  };
  
  // Handle completion
  currentXHR.onload = function() {
    if (currentXHR.status === 200) {
      updateProgress(100);
      progressText.textContent = 'Upload complete!';
      resetForm();
    } else {
      handleError('Upload failed: ' + currentXHR.statusText);
    }
  };
  
  // Handle errors
  currentXHR.onerror = () => handleError('Network error occurred');
  currentXHR.onabort = () => handleError('Upload cancelled');
  
  // Send request
  currentXHR.open('POST', '/api/upload', true);
  currentXHR.send(formData);
  
  // Enable cancel button
  cancelBtn.disabled = false;
}

function updateProgress(percent) {
  progressBar.value = percent;
  progressText.textContent = `${percent}% uploaded`;
}

function handleError(message) {
  progressText.textContent = message;
  progressBar.value = 0;
  resetForm();
}

function resetForm() {
  cancelBtn.disabled = true;
  currentXHR = null;
  setTimeout(() => {
    progressBar.value = 0;
    progressText.textContent = '0% uploaded';
  }, 2000);
}

// Cancel upload functionality
cancelBtn.addEventListener('click', () => {
  if (currentXHR) {
    currentXHR.abort();
  }
});

理解进度事件

xhr.upload.onprogress 事件提供三个关键属性:

  • event.loaded: 已上传的字节数
  • event.total: 文件总大小(字节)
  • event.lengthComputable: 布尔值,指示总大小是否已知

lengthComputable 为 true 时,按 (loaded / total) * 100 计算百分比。当为 false 时,服务器没有提供 Content-Length 头,因此通过移除进度元素的 value 属性来显示不确定的进度状态。

样式优化用户体验

添加 CSS 使上传进度实现更加清晰可见:

progress {
  width: 100%;
  height: 24px;
  margin: 10px 0;
}

/* Webkit browsers */
progress::-webkit-progress-bar {
  background-color: #f0f0f0;
  border-radius: 4px;
}

progress::-webkit-progress-value {
  background-color: #4CAF50;
  border-radius: 4px;
  transition: width 0.3s ease;
}

/* Firefox */
progress::-moz-progress-bar {
  background-color: #4CAF50;
  border-radius: 4px;
}

#progressText {
  display: block;
  margin-top: 5px;
  font-weight: 600;
}

服务器端注意事项

服务器端点应该接受 multipart/form-data 上传。以下是使用 Express 和 Multer 的最小化 Node.js 示例:

const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/api/upload', upload.single('file'), (req, res) => {
  // File is available as req.file
  res.json({ success: true, filename: req.file.filename });
});

对于生产环境,需要添加文件类型验证、病毒扫描和适当的存储处理(本地文件系统或云服务如 AWS S3)。

浏览器兼容性和渐进增强

这种方法在所有现代浏览器中都能工作,因为 XMLHttpRequest Level 2(包括上传进度)自以下版本开始就得到支持:

  • Chrome 7+
  • Firefox 3.5+
  • Safari 5+
  • Edge(所有版本)

对于较旧的浏览器,上传仍然有效——用户只是看不到进度更新。表单会优雅降级为标准文件上传。

总结

构建可访问的上传进度条需要使用 XMLHttpRequest 来获取进度事件、使用语义化 HTML 来构建结构,以及使用周到的 JavaScript 来处理各种上传状态。这个实现提供了适用于所有用户(包括使用辅助技术的用户)的实时反馈,同时保持广泛的浏览器兼容性,无需任何外部库。

常见问题

Fetch API 不支持上传进度事件。XMLHttpRequest 专门提供了 xhr.upload.onprogress 事件处理程序来跟踪上传进度,使其成为实时进度条的唯一可行选项。

为每个文件创建单独的 XMLHttpRequest 实例或按顺序排队上传。跟踪单个进度,并通过平均百分比或汇总所有文件的已加载字节来计算总体进度。

没有 Content-Length 头时,event.lengthComputable 返回 false。通过移除进度元素的 value 属性并显示通用加载文本而不是百分比来显示不确定的进度状态。

标准的 XMLHttpRequest 不支持可恢复上传。要实现此功能,需要实现带有服务器端支持的分块上传,或使用处理文件分割和恢复逻辑的专用库。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay