使用Fetch接口跟踪文件上传进度
在前端开发场景中,文件上传是常见需求,很多场景下还需要实时展示上传进度,提升用户体验。传统的文件上传通常会使用XMLHttpRequest实现进度监听,不过随着Fetch API的普及,很多开发者也希望借助Fetch完成相关功能。但实际上,原生的Fetch API本身并不支持进度跟踪,我们需要结合ReadableStream和手动处理请求体来实现上传进度的监控。
为什么原生Fetch无法直接跟踪上传进度
原生Fetch API的设计更加简洁,专注于请求和响应的核心逻辑,并没有提供类似XMLHttpRequest的onprogress事件回调。它的请求体在被发送时,不会对外暴露已上传的字节数信息,因此无法直接获取上传进度。如果要使用Fetch实现进度跟踪,我们需要对上传的数据流进行手动封装,实时计算已发送的数据量。
实现思路
核心思路是将待上传的文件数据转换为可追踪的流,在流的处理过程中记录已发送的字节数,再通过回调或者状态更新将进度信息同步到页面上。具体步骤如下:
获取需要上传的文件对象,计算文件总大小
将文件数据封装为可追踪的
ReadableStream,在流的分块发送过程中累计已发送的字节数使用Fetch发送封装后的请求体,同时通过定时器或者回调实时更新进度状态
完整实现示例
以下是一个基于原生Fetch和ReadableStream实现文件上传进度跟踪的完整示例,假设上传接口地址为https://www.ipipp.com/api/upload:
// 上传进度回调函数类型定义
/**
* @param {number} progress 上传进度,范围0-100
* @param {number} loaded 已上传字节数
* @param {number} total 总字节数
*/
function uploadFileWithProgress(file, onProgress) {
const totalSize = file.size;
let loadedSize = 0;
// 创建可读流,分块读取文件并累计已上传大小
const readableStream = new ReadableStream({
start(controller) {
const reader = file.stream().getReader();
// 递归读取文件流
function read() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// 累计已上传字节数
loadedSize += value.length;
// 计算进度并回调
const progress = Math.round((loadedSize / totalSize) * 100);
onProgress(progress, loadedSize, totalSize);
// 将分块数据放入流中
controller.enqueue(value);
read();
});
}
read();
}
});
// 发起Fetch请求
return fetch('https://www.ipipp.com/api/upload', {
method: 'POST',
body: readableStream,
headers: {
'Content-Type': file.type || 'application/octet-stream'
}
}).then(response => {
if (!response.ok) {
throw new Error('上传失败,状态码:' + response.status);
}
return response.json();
});
}
// 使用示例
const fileInput = document.querySelector('#fileInput');
const progressBar = document.querySelector('#progressBar');
const progressText = document.querySelector('#progressText');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadFileWithProgress(file, (progress, loaded, total) => {
// 更新进度展示
progressBar.value = progress;
progressText.textContent = `上传进度:${progress}%,已上传${loaded}字节,总大小${total}字节`;
});
console.log('上传成功,返回结果:', result);
alert('文件上传成功');
} catch (error) {
console.error('上传出错:', error);
alert('文件上传失败:' + error.message);
}
});对应的HTML结构示例:
<input type="file" id="fileInput" /> <progress id="progressBar" value="0" max="100"></progress> <p id="progressText">上传进度:0%</p>
注意事项
该实现依赖
ReadableStream和file.stream()API,需要确保运行环境支持这些特性,现代浏览器基本都已经支持,如果需要兼容旧环境可能需要额外做polyfill处理。由于Fetch本身不会拦截请求体的发送过程,进度的计算是基于流的分块处理,和实际网络发送可能存在极小的偏差,对于大多数业务场景来说可以忽略。
如果服务端接口有额外的参数要求,比如需要携带token、其他表单字段,可以在Fetch的headers中添加对应参数,或者将额外参数和文件流合并处理后再发送。
上传大文件时建议结合服务端的分片上传接口使用,避免单个请求体积过大导致超时或者失败,此时进度计算可以基于分片的完成情况累加实现。
替代方案
如果不需要强制使用Fetch接口,XMLHttpRequest是更简单的实现进度跟踪的选择,它原生支持onprogress事件,代码更加简洁。以下是XMLHttpRequest实现上传进度的简单示例:
function uploadFileWithXHR(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.ipipp.com/api/upload');
// 监听上传进度
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const progress = Math.round((e.loaded / e.total) * 100);
onProgress(progress, e.loaded, e.total);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('上传失败,状态码:' + xhr.status));
}
};
xhr.onerror = () => reject(new Error('网络错误,上传失败'));
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}可以根据实际项目需求选择合适的实现方式,如果项目已经统一使用Fetch进行请求处理,那么可以通过上述的流封装方式实现进度跟踪;如果更看重实现简单性,XMLHttpRequest是更优的选择。