前端文件上传处理:从基础到分块上传实战
文件上传看似简单,实则涉及用户体验、性能优化和安全保障等多个层面。仅仅依赖原生的 <input type="file"> 往往无法满足生产环境的需求。本文将深入探讨如何构建健壮的文件上传方案。
常见误区
很多开发者在实现上传时容易忽略以下关键点:
- 缺乏验证:未在前端校验文件大小和类型,导致无效请求到达服务器,浪费带宽。
- 无进度反馈:用户无法感知上传状态,长时间等待容易导致页面误以为卡死。
- 错误处理缺失:网络波动或服务器异常时,没有明确的提示,用户无从下手。
- 内存溢出风险:直接通过
FormData上传超大文件可能导致浏览器内存不足。
基础上传实现
首先,我们需要确保文件符合基本规范。在发送请求前进行客户端校验是最有效的防线。
function validateAndUpload(file) {
// 限制文件大小为 10MB
if (file.size > 10 * 1024 * 1024) {
alert('File too large');
return;
}
// 限制文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert('Invalid file type');
return;
}
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('Upload failed');
return response.json();
})
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
}
注意这里使用了 Promise 链式调用,便于统一处理错误。对于多文件上传,逻辑类似,只需遍历文件列表即可。
上传进度监控
原生 fetch API 并不支持上传进度监听。如果需要精确的进度条,推荐使用 XMLHttpRequest。
function uploadWithProgress(file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded * 100) / event.total);
// 更新 UI 进度条
document.getElementById('progressBar').style.width = percent + '%';
}
});
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Completed');
} else {
console.error('Failed');
}
};
xhr.open('POST', '/api/upload');
xhr.send(formData);
}
通过监听 xhr.upload 事件,我们可以实时获取已传输字节数,从而提供流畅的用户体验。
拖拽与大文件分片
现代浏览器支持 HTML5 Drag and Drop API,允许用户直接将文件拖入指定区域。同时,对于超过 100MB 的大文件,分块上传(Chunk Upload)是必要的策略。
拖拽上传
function setupDragDrop() {
const dropArea = document.getElementById('dropArea');
dropArea.addEventListener('dragover', e => {
e.preventDefault();
dropArea.classList.add('drag-over');
});
dropArea.addEventListener('drop', e => {
e.preventDefault();
dropArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
// 调用上传逻辑
uploadFiles(files);
});
}
分块上传
async function uploadLargeFile(file) {
const chunkSize = 1024 * 1024; // 1MB per chunk
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = generateId();
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
await fetch('/api/upload-chunk', { method: 'POST', body: formData });
}
// 通知服务器合并分片
await fetch('/api/merge-chunks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId, fileName: file.name, totalChunks })
});
}
分片上传不仅能提高成功率,还能支持断点续传。如果某个分片失败,只需重新上传该分片,而无需重头开始。
最佳实践建议
除了核心上传逻辑,还有一些细节值得注意:
- 图片压缩:在上传前利用 Canvas 对图片进行压缩,减少流量消耗。
- 安全校验:不要仅依赖前端校验,后端必须再次验证文件扩展名和内容。
- 状态管理:维护一个上传任务队列,方便用户取消或重试。
记住,文件上传的目的是为了服务业务,而非炫技。根据实际场景选择最合适的方案,平衡复杂度与功能需求,才是成熟工程师的做法。
