实现下载进度

一、 onprogress 讲解

onprogress 是一个监听器,用于监听 progress 事件。这个事件在下载/上传过程中多次触发,可以用于更新进度条、显示加载状态等。

用途 API 使用方式
下载进度 xhr.onprogress xhr.onprogress = function (e) {}
上传进度 xhr.upload.onprogress xhr.upload.onprogress = function (e) {}
Fetch 进度 ReadableStream response.body.getReader()
媒体缓冲 <video>.onprogress video.onprogress = function () {}

1.1 下载进度xhr.onprogress

js 复制代码
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://example.com/largefile.zip", true);

xhr.onprogress = function (event) {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100;
    console.log(`下载进度:${percent.toFixed(2)}%`);
  } else {
    console.log(`已加载:${event.loaded} 字节(总大小未知)`);
  }
};

xhr.onload = function () {
  console.log("下载完成!");
};

xhr.send();

1.2 上传进度xhr.upload.onprogress

html 复制代码
<body>
  <h2>上传文件</h2>
  <input type="file" id="file-input" />
  <button id="upload-btn">上传</button>

  <div id="progress-container">
    <div id="progress-bar"></div>
  </div>
  <div id="progress-text">上传进度:0%</div>

  <script>
    document.getElementById('upload-btn').addEventListener('click', () => {
      const fileInput = document.getElementById('file-input');
      const file = fileInput.files[0];

      if (!file) {
        alert("请选择一个文件");
        return;
      }

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

      // 上传进度
      xhr.upload.onprogress = function (event) {
        if (event.lengthComputable) {
          const percent = (event.loaded / event.total * 100).toFixed(2);
          document.getElementById("progress-bar").style.width = percent + "%";
          document.getElementById("progress-text").textContent = `上传进度:${percent}%`;
        }
      };

      xhr.onload = function () {
        if (xhr.status === 200) {
          document.getElementById("progress-text").textContent = "上传完成!";
        } else {
          document.getElementById("progress-text").textContent = "上传失败:" + xhr.statusText;
        }
      };

      xhr.onerror = function () {
        document.getElementById("progress-text").textContent = "上传出错!";
      };

      xhr.open("POST", "https://httpbin.org/post"); // 测试上传地址,可换成你自己的
      xhr.send(formData);
    });
  </script>
</body>

1.3 Fetch 进度 ReadableStream

Fetch 本身不提供 onprogress,但可以通过 ReadableStream 来间接实现:

js 复制代码
fetch('https://example.com/largefile.zip')
  .then(response => {
    const reader = response.body.getReader();
    const contentLength = +response.headers.get('Content-Length');

    let received = 0;
    const chunks = [];

    return reader.read().then(function process({ done, value }) {
      if (done) {
        console.log("下载完成!");
        return;
      }

      chunks.push(value);
      received += value.length;
      console.log(`接收进度: ${(received / contentLength * 100).toFixed(2)}%`);

      return reader.read().then(process);
    });
  });

1.4 媒体缓冲 <video>.onprogress

html 复制代码
<video src="video.mp4" controls onprogress="console.log('缓冲中...')"></video>
js 复制代码
video.onprogress = () => {
  const buffered = video.buffered;
  if (buffered.length) {
    const end = buffered.end(buffered.length - 1);
    const percent = (end / video.duration) * 100;
    console.log(`缓冲进度: ${percent.toFixed(2)}%`);
  }
};

二、服务端支持下载时的进度条

以下是 Node.js 服务端在下载文件时分别使用:

  • Content-Length(固定长度,支持百分比进度)
  • Transfer-Encoding: chunked(流式传输,无法预知总长度)

2.1 Content-Length

js 复制代码
// server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const cors = require('cors');

const app = express();
const PORT = 3000;

app.use(cors()); // 允许跨域

app.get('/download/content-length', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'bigfile.zip');
  const stat = fs.statSync(filePath); // 获取文件大小

  res.writeHead(200, {
    'Content-Type': 'application/zip',
    'Content-Disposition': 'attachment; filename="bigfile.zip"',
    'Content-Length': stat.size // 🎯 关键设置
  });

  const readStream = fs.createReadStream(filePath);
  readStream.pipe(res);
});

app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

2.2 Transfer-Encoding: chunked

默认只要不设置 Content-Length,Node.js 会启用 chunked 传输编码。

js 复制代码
app.get('/download/chunked', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'bigfile.zip');

  res.writeHead(200, {
    'Content-Type': 'application/zip',
    'Content-Disposition': 'attachment; filename="chunked-bigfile.zip"',
    // ❌ 不设置 Content-Length
    // 会自动使用 Transfer-Encoding: chunked
  });

  const readStream = fs.createReadStream(filePath);
  readStream.pipe(res);
});

三、 其他问题

3.1 content-type与 Accept

特性 Accept (请求头) Content-Type (实体头)
作用方向 客户端 → 服务器 双向 (请求或响应中均可出现)
核心目的 告诉服务器客户端能处理什么格式 告诉接收方实际发送的数据是什么格式
所属阶段 请求阶段 (期望值) 数据传输阶段 (实际值)
协商角色 发起内容协商 声明当前传输内容的类型
位置场景 只出现在请求头 可出现在请求头响应头

3.2 responseType 与 Accept的区别

  1. Accept 是给服务器看的协商请求头:

    • 你(客户端)说:"嗨服务器,我更喜欢 JSON 格式的数据,但如果不行,给我 HTML 或纯文本也行 (Accept: application/json, text/html, text/plain)。"
    • 服务器看到这个请求头,结合它能提供的资源格式,决定最终返回哪种格式(通过 Content-Type 响应头告知)。
    • 它影响的是服务器选择发送什么
  2. responseType 是给浏览器/客户端运行时看的处理指令:

    • 你(开发者)对浏览器说:"不管服务器实际返回了什么 Content-Type,也不管我请求头里写了什么 Accept当响应体的字节流到达时,你(浏览器)必须把它当作 [某种类型] 来解析处理,并按照对应的格式提供给我(JavaScript)。 "
    • 它影响的是客户端如何解释和处理接收到的原始响应字节流。
    • 例如:设置 xhr.responseType = 'arraybuffer' 告诉浏览器:"把响应体直接给我存成一个 ArrayBuffer 对象,不要尝试解析成字符串或 JSON"。即使服务器返回了 Content-Type: application/jsonxhr.response 也不会是一个 JavaScript 对象,而是一个包含 JSON 字符串原始字节的 ArrayBuffer

3.3 常见 responseType

常见 responseType 值(XMLHttpRequest / Fetch API)

responseType 返回值类型 适用场景 Fetch API 等效操作
"" (默认) string 文本响应(HTML/XML/纯文本) response.text()
"text" string 明确要求文本响应 response.text()
"json" JavaScript 对象 JSON API 响应(自动解析为对象) response.json()
"arraybuffer" ArrayBuffer 二进制数据(图片/音频/自定义二进制格式) response.arrayBuffer()
"blob" Blob 文件类数据(图片/PDF/Excel) response.blob()
"document" Document 对象 XML/HTML 文档(可直接操作 DOM) 无直接等效,需用 tex

必须设置 responseType 的场景

1️⃣ 处理二进制文件(强制设置)

必须使用: "blob""arraybuffer"

ini 复制代码
const xhr = new XMLHttpRequest();
xhr.open('GET', '/image.png');
xhr.responseType = 'blob'; // 必须设置

xhr.onload = () => {
  const blob = xhr.response;
  const img = document.createElement('img');
  img.src = URL.createObjectURL(blob);
  document.body.appendChild(img);
};
xhr.send();

为什么必须设置?

如果未设置,浏览器会将二进制数据尝试解码为 UTF-8 文本,导致数据损坏。

2️⃣ 处理大文件流(强制设置)

必须使用: "arraybuffer" 分块处理

ini 复制代码
// 分块读取大文件
xhr.responseType = 'arraybuffer';
xhr.onprogress = (event) => {
  const chunk = new Uint8Array(xhr.response, event.loaded);
  // 处理分块数据...
};
3️⃣ 强制解析 JSON(推荐设置)

推荐使用: "json"

javascript 复制代码
// Fetch API 示例
fetch('/api/data')
  .then(response => response.json()) // 等效 responseType=json
  .then(data => console.log(data));

为什么推荐?

避免手动 JSON.parse() 的安全风险(如解析失败导致阻塞)。

总结:必须设置 responseType 的场景

  1. 所有二进制数据处理(图片/音视频/文件)

    • → 必须用 blobarraybuffer
  2. 流式数据处理/分块加载

    • → 必须用 arraybuffer
  3. WebAssembly 文件加载

    • → 必须用 arraybuffer

📌 黄金法则:
只要响应不是纯文本/JSON/XML,就必须显式设置 responseType

浏览器不会自动正确处理二进制数据,未设置将导致不可逆数据损坏

3.4 transfer-Encoding:chunked 与content-length

只有设置了 Content-Length 的响应,前端才能显示"下载百分比";否则就只能显示"已下载多少字节"。

核心区别总结

项目 Content-Length Transfer-Encoding: chunked
是否可计算总大小? ✅ 是,前端能知道总字节数 ❌ 否,前端无法预知总大小
是否支持显示百分比进度? ✅ 可以显示百分比(如 65%) ❌ 只能显示已接收字节,无法显示百分比
用途 固定大小内容,如文件下载 动态生成内容、流式传输
前端进度显示体验 👍 精准 👎 模糊(只能用"下载中...")

3.5 content-Disposition

Content-Disposition 是 HTTP 响应头的一部分,用于告诉浏览器如何处理收到的内容:是直接在页面显示,还是作为附件下载。

js 复制代码
Content-Disposition: inline
Content-Disposition: attachment; filename="example.txt"
类型 含义
inline 在页面中直接展示(默认值)
attachment 提示浏览器下载,而不是直接打开
filename 指定下载保存时的默认文件名

欢迎关注我的前端自检清单,我和你一起成长

相关推荐
海的诗篇_1 小时前
前端开发面试题总结-原生小程序部分
前端·javascript·面试·小程序·vue·html
黄瓜沾糖吃2 小时前
大佬们指点一下倒计时有什么问题吗?
前端·javascript
温轻舟2 小时前
3D词云图
前端·javascript·3d·交互·词云图·温轻舟
浩龙不eMo3 小时前
✅ Lodash 常用函数精选(按用途分类)
前端·javascript
爱分享的程序员3 小时前
前端面试专栏-算法篇:17. 排序算法
前端·javascript·node.js
pe7er3 小时前
使用 Vue 官方脚手架创建项目时遇到 Node 18 报错问题的排查与解决
前端·javascript·vue.js
pe7er4 小时前
使用 types / typings 实现全局 TypeScript 类型定义,无需 import/export
前端·javascript·vue.js
islandzzzz4 小时前
(第二篇)HMTL+CSS+JS-新手小白循序渐进案例入门
前端·javascript·css·html
喝拿铁写前端4 小时前
前端实战优化:在中后台系统中用语义化映射替代 if-else,告别魔法数字的心智负担
前端·javascript·架构
超人不会飛5 小时前
就着HTTP聊聊SSE的前世今生
前端·javascript·http