一、 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的区别
-
Accept
是给服务器看的协商请求头:- 你(客户端)说:"嗨服务器,我更喜欢 JSON 格式的数据,但如果不行,给我 HTML 或纯文本也行 (
Accept: application/json, text/html, text/plain
)。" - 服务器看到这个请求头,结合它能提供的资源格式,决定最终返回哪种格式(通过
Content-Type
响应头告知)。 - 它影响的是服务器选择发送什么。
- 你(客户端)说:"嗨服务器,我更喜欢 JSON 格式的数据,但如果不行,给我 HTML 或纯文本也行 (
-
responseType
是给浏览器/客户端运行时看的处理指令:- 你(开发者)对浏览器说:"不管服务器实际返回了什么
Content-Type
,也不管我请求头里写了什么Accept
,当响应体的字节流到达时,你(浏览器)必须把它当作 [某种类型] 来解析处理,并按照对应的格式提供给我(JavaScript)。 " - 它影响的是客户端如何解释和处理接收到的原始响应字节流。
- 例如:设置
xhr.responseType = 'arraybuffer'
告诉浏览器:"把响应体直接给我存成一个ArrayBuffer
对象,不要尝试解析成字符串或 JSON"。即使服务器返回了Content-Type: application/json
,xhr.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 的场景
-
所有二进制数据处理(图片/音视频/文件)
- → 必须用
blob
或arraybuffer
- → 必须用
-
流式数据处理/分块加载
- → 必须用
arraybuffer
- → 必须用
-
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 |
指定下载保存时的默认文件名 |
欢迎关注我的前端自检清单,我和你一起成长