前端文件上传方案概述
为什么需要优化文件上传
原生 input 标签在处理大文件时存在明显局限,例如上传 100MB 的文件可能导致浏览器卡死且无进度提示。为了提升用户体验和传输稳定性,需要采用更完善的上传方案。
原生上传示例
<!-- 反面教材:原生文件上传 -->
<input type="file" onchange="uploadFile(this.files[0])" />
<script>
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
// 直接上传,没有进度,没有断点续传
fetch('/api/upload', {
method: 'POST',
body: formData
});
}
</script>
核心实现方案
1. 分片上传
将大文件切割成多个小块并发或顺序上传,降低单次请求压力并支持进度追踪。
// 正确姿势:分片上传
class ChunkUploader {
constructor(file, options = {}) {
this.file = file;
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
this.chunks = Math.ceil(file.size / this.chunkSize);
this.uploadedChunks = 0;
}
async upload() {
const promises = [];
for (let i = 0; i < this.chunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
promises.push(this.uploadChunk(chunk, i));
}
await Promise.all(promises);
await this.mergeChunks();
}
async uploadChunk(chunk, index) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('total', this.chunks);
formData.append('filename', this.file.name);
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
this.uploadedChunks++;
this.onProgress(this.uploadedChunks / this.chunks);
}
onProgress(progress) {
console.log(`上传进度:${(progress * 100).toFixed(2)}%`);
}
async mergeChunks() {
await fetch('/api/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: this.file.name, chunks: this.chunks })
});
}
}
// 使用
const uploader = new ChunkUploader(file, { chunkSize: 1024 * 1024 });
uploader.upload();
2. 断点续传
记录已上传的分片索引,网络中断后可从上次位置继续,避免重复传输。
// 正确姿势:断点续传
class ResumableUploader {
constructor(file) {
this.file = file;
this.chunkSize = 1024 * 1024;
this.uploadedChunks = new Set();
}
async init() {
// 获取已上传的分片
const response = await fetch(`/api/upload/status?filename=${this.file.name}`);
const { uploadedChunks } = await response.json();
this.uploadedChunks = new Set(uploadedChunks);
}
async upload() {
const chunks = Math.ceil(this.file.size / this.chunkSize);
for (let i = 0; i < chunks; i++) {
if (this.uploadedChunks.has(i)) {
console.log(`分片${i}已上传,跳过`);
continue;
}
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
await this.uploadChunk(chunk, i);
this.uploadedChunks.add(i);
localStorage.setItem('uploadProgress', JSON.stringify([...this.uploadedChunks]));
}
await this.mergeChunks();
localStorage.removeItem('uploadProgress');
}
async uploadChunk(chunk, index) {
// 上传逻辑...
}
}
3. 拖拽上传
利用 HTML5 Drag and Drop API 提供直观的文件选择体验,结合 React Hooks 实现组件化。
// 正确姿势:拖拽上传
import { useCallback } from 'react';
function DragUpload({ onUpload }) {
const handleDrop = useCallback((e) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
files.forEach(file => onUpload(file));
}, [onUpload]);
const handleDragOver = useCallback((e) => {
e.preventDefault();
}, []);
return (
<div className="drag-upload" onDrop={handleDrop} onDragOver={handleDragOver}>
<p>拖拽文件到此处上传</p>
<input type="file" multiple onChange={(e) => {
Array.from(e.target.files).forEach(file => onUpload(file));
}} />
</div>
);
}
实战技巧:文件上传指南
1. 上传优化策略
- 分片上传:大文件切分上传,提高成功率。
- 断点续传:支持暂停恢复,节省流量和时间。
- 并发控制:限制同时上传数量,防止阻塞主线程。
- 进度显示:实时显示上传进度条,提升用户感知。
2. 最佳实践
// ✅ 显示上传进度
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
const progress = (e.loaded / e.total) * 100;
console.log(`${progress}%`);
};
// ✅ 图片预览
const preview = URL.createObjectURL(file);
// ✅ 文件类型检查
const allowedTypes = ['image/jpeg', 'image/png'];
if (!allowedTypes.includes(file.type)) {
alert('不支持的文件类型');
return;
}
总结
文件上传是用户体验的关键环节。通过分片、断点续传及拖拽等优化手段,可以显著提升上传效率和专业度,避免原生 input 带来的性能瓶颈。


