如何使用Fetch API跟踪文件上传进度
在前端开发场景中,文件上传是常见需求,传统实现通常依赖XMLHttpRequest的原生进度事件,而Fetch API作为现代浏览器提供的网络请求接口,本身是默认不支持进度跟踪的。不过我们可以通过结合ReadableStream和手动构造请求体的方式,实现Fetch API上传文件时的进度监控。
实现原理
Fetch API的Request对象支持传入ReadableStream作为请求体,我们可以将文件数据拆分为多个小块,在逐块发送的过程中统计已发送的数据量,从而计算出上传进度。核心思路分为三步:
获取需要上传的文件对象,得到文件总大小
创建一个可读流,按固定大小拆分文件数据,每发送一块就更新已上传的字节数
将可读流作为Fetch请求的请求体,结合总大小计算实时上传进度
完整实现代码
以下是基于原生JavaScript的实现示例,兼容现代浏览器环境:
// 上传进度回调函数类型定义
/**
* @typedef {Object} UploadProgress
* @property {number} loaded - 已上传字节数
* @property {number} total - 文件总字节数
* @property {number} percent - 上传百分比(0-100)
*/
/**
* 使用Fetch API上传文件并跟踪进度
* @param {File} file - 要上传的文件对象
* @param {string} uploadUrl - 上传接口地址,示例:https://www.ipipp.com/api/upload
* @param {Function} onProgress - 进度回调函数,接收UploadProgress参数
* @returns {Promise<Response>} 上传请求的响应结果
*/
async function uploadFileWithProgress(file, uploadUrl, onProgress) {
const chunkSize = 64 * 1024; // 每块64KB
const totalSize = file.size;
let uploadedSize = 0;
// 创建可读流,分块读取文件并发送
const readableStream = new ReadableStream({
async start(controller) {
const reader = file.stream().getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
controller.close();
break;
}
// 发送当前块数据
controller.enqueue(value);
// 更新已上传大小
uploadedSize += value.length;
// 触发进度回调
const percent = totalSize > 0 ? Math.round((uploadedSize / totalSize) * 100) : 0;
onProgress({
loaded: uploadedSize,
total: totalSize,
percent: percent
});
}
} catch (error) {
controller.error(error);
} finally {
reader.releaseLock();
}
}
});
// 发起Fetch上传请求
const response = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Content-Type': file.type || 'application/octet-stream',
'Content-Length': totalSize.toString()
},
body: readableStream
});
return response;
}使用示例
下面是实际调用上述函数的示例,包含进度展示和结果处理:
// 获取页面元素
const fileInput = document.querySelector('#fileInput');
const progressBar = document.querySelector('#progressBar');
const progressText = document.querySelector('#progressText');
const uploadBtn = document.querySelector('#uploadBtn');
// 点击上传按钮触发上传
uploadBtn.addEventListener('click', async () => {
const file = fileInput.files[0];
if (!file) {
alert('请先选择要上传的文件');
return;
}
// 重置进度展示
progressBar.value = 0;
progressText.textContent = '0%';
try {
const response = await uploadFileWithProgress(
file,
'https://www.ipipp.com/api/upload',
(progress) => {
// 更新进度条和文本
progressBar.value = progress.percent;
progressText.textContent = `${progress.percent}% (${progress.loaded}/${progress.total} 字节)`;
}
);
if (response.ok) {
const result = await response.json();
alert('文件上传成功');
console.log('上传结果:', result);
} else {
alert(`上传失败,状态码:${response.status}`);
}
} catch (error) {
alert('上传过程发生错误');
console.error('上传错误:', error);
}
});注意事项
该实现依赖
ReadableStream和File.prototype.stream()API,需要浏览器支持,低版本浏览器(如IE)无法使用,生产环境可根据需求添加polyfill或者降级到XMLHttpRequest实现。部分服务端可能对请求头的
Content-Length有校验,如果服务端不支持流式传输的Transfer-Encoding: chunked,需要确认是否兼容分块发送的请求体。如果上传大文件,建议适当调大
chunkSize减少进度回调的触发次数,避免频繁更新UI造成性能问题。实际项目中需要根据服务端接口要求调整请求头,比如添加认证token、修改
Content-Type等。
与XMLHttpRequest的对比
传统XMLHttpRequest原生支持progress事件,使用更简单,而Fetch API的实现方式稍复杂,但具备更好的流式处理能力,适合需要结合其他流操作(如边读取边压缩、边转换数据边上传)的场景。如果仅需简单的进度跟踪,也可以根据项目技术栈选择合适的方案。