使用Vue-cli时,Vue2怎样实现大文件的分块上传与断点续传?
100块预算还要兼容IE9?甲方是不是对程序员有什么误解?20G文件上传?建议先问问甲方他们服务器硬盘够不够大7x24小时免费技术支持?我连7x24小时睡觉都保证不了…要源代码?要文档?要一条龙服务?100块连个外卖都点不了好吗!加群374992201领红包?兄弟,有这功夫不如多接几个项目…不过既然你都看到这里了,代码拿去用吧,记得请我喝奶茶(至少得是喜茶级别的)!
大文件上传解决方案
各位同行大佬们好,作为一个在广东摸爬滚打多年的前端"老油条",最近接了个让我差点秃顶的项目——20G大文件上传系统,还要兼容IE9!这感觉就像让我用竹篮子去打水还要不漏一样刺激…
需求分析:客户这是要我老命啊
- 20G大文件传输:我寻思着这不是上传,这是在往浏览器里塞一头大象啊
- 文件夹保留层级:客户说文件夹里有1000个分类文件,这哪是文件夹,这是个文件博物馆!
- 加密传输存储:SM4、AES齐上阵,比瑞士银行的保险箱还严实
- 断点续传:关了浏览器、重启电脑都不能丢进度,这要求比我的记忆力靠谱多了
- 非打包下载:几万个文件直接下载,这网速得比我的工资涨得还快才行
- 兼容IE9:Windows7+IE9的组合,让我梦回2012年,青春啊!
最绝的是预算100元以内还要求7×24小时支持,这价格连我家的路由器月租都不够啊兄弟们!
前端解决方案:与IE9的世纪和解
既然客户爸爸说了要用原生JS,那咱们就用H5的File API+IndexedDB来整活:
穷逼版大文件上传
/* 祖传CSS,兼容IE9 */
.upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin: 20px;
background: #f9f9f9;
}
.progress-container {
width: 100%;
background-color: #f5f5f5;
margin: 10px 0;
height: 20px;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
width: 0%;
transition: width 0.3s;
}
.progress-text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: #333;
font-size: 12px;
}
.file-item {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
background: #fff;
}
.file-name {
font-weight: bold;
}
.file-path {
color: #666;
font-size: 0.9em;
margin-left: 10px;
}
.file-size {
color: #888;
font-size: 0.9em;
margin-left: 10px;
}
.file-controls {
margin-top: 5px;
}
button {
padding: 5px 10px;
margin-right: 5px;
background: #f0f0f0;
border: 1px solid #ddd;
cursor: pointer;
}
button:hover {
background: #e0e0e0;
}
大文件上传(兼容IE9版)
拖放文件或文件夹到此处
或
选择文件/文件夹
等待上传文件...
开始上传
暂停
继续
加密方式:
SM4国密
AES-256
// 上传队列
var uploadQueue = [];
var currentUpload = null;
var chunkSize = 5 * 1024 * 1024; // 5MB分块
// 初始化
function init() {
// 文件选择处理
document.getElementById('fileInput').addEventListener('change', function(e) {
handleFiles(e.target.files);
});
// 拖放处理
var dropArea = document.getElementById('dropArea');
dropArea.addEventListener('dragover', function(e) {
e.preventDefault();
dropArea.style.borderColor = '#4CAF50';
dropArea.style.background = '#f0fff0';
});
dropArea.addEventListener('dragleave', function() {
dropArea.style.borderColor = '#ccc';
dropArea.style.background = '#f9f9f9';
});
dropArea.addEventListener('drop', function(e) {
e.preventDefault();
dropArea.style.borderColor = '#ccc';
dropArea.style.background = '#f9f9f9';
handleFiles(e.dataTransfer.files);
});
// 加载未完成的传输
loadPendingTransfers();
}
// 处理文件选择
function handleFiles(files) {
var queueContainer = document.getElementById('uploadQueue');
queueContainer.innerHTML = '';
for (var i = 0; i < files.length; i++) {
var file = files[i];
addFileToQueue(file);
}
}
// 添加文件到队列
function addFileToQueue(file) {
var fileItem = {
id: generateFileId(file),
name: file.name,
path: file.webkitRelativePath || '',
size: file.size,
progress: 0,
status: 'pending',
file: file
};
uploadQueue.push(fileItem);
renderQueue();
}
// 渲染队列
function renderQueue() {
var queueContainer = document.getElementById('uploadQueue');
queueContainer.innerHTML = '';
if (uploadQueue.length === 0) {
queueContainer.innerHTML = '<p>等待上传文件...</p>';
return;
}
for (var i = 0; i < uploadQueue.length; i++) {
var item = uploadQueue[i];
var itemElement = document.createElement('div');
itemElement.className = 'file-item';
itemElement.innerHTML = `
<div>
<span class="file-name">${item.name}</span>
<span class="file-path">${item.path}</span>
<span class="file-size">${formatFileSize(item.size)}</span>
</div>
<div class="progress-container">
<div class="progress-bar" style="width:${item.progress}%"></div>
<span class="progress-text">${item.progress}%</span>
</div>
<div class="file-controls">
<button onclick="pauseItem('${item.id}')" ${item.status !== 'uploading' ? 'disabled' : ''}>暂停</button>
<button onclick="resumeItem('${item.id}')" ${item.status !== 'paused' ? 'disabled' : ''}>继续</button>
<button onclick="cancelItem('${item.id}')">取消</button>
</div>
`;
queueContainer.appendChild(itemElement);
}
}
// 开始上传
function startUpload() {
if (uploadQueue.length === 0) return;
currentUpload = uploadQueue.find(item => item.status === 'pending');
if (currentUpload) {
currentUpload.status = 'uploading';
uploadFile(currentUpload);
}
}
// 上传文件
function uploadFile(fileItem) {
var file = fileItem.file;
var totalChunks = Math.ceil(file.size / chunkSize);
// 从本地存储加载断点
var resumeChunk = localStorage.getItem('resume_' + fileItem.id) || 0;
// 上传分块
for (var chunkIndex = resumeChunk; chunkIndex < totalChunks; chunkIndex++) {
if (fileItem.status === 'paused') break;
var start = chunkIndex * chunkSize;
var end = Math.min(start + chunkSize, file.size);
var chunk = file.slice(start, end);
var formData = new FormData();
formData.append('fileId', fileItem.id);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('fileName', fileItem.name);
formData.append('filePath', fileItem.path);
formData.append('fileSize', fileItem.size);
formData.append('chunkData', chunk);
formData.append('encryption', document.getElementById('encryptionType').value);
formData.append('encryptionKey', document.getElementById('encryptionKey').value);
// AJAX上传(兼容IE9)
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload/chunk', false); // 同步上传
xhr.upload.onprogress = function(e) {
var loaded = chunkIndex * chunkSize + e.loaded;
fileItem.progress = Math.round((loaded / fileItem.size) * 100);
renderQueue();
};
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 保存断点
localStorage.setItem('resume_' + fileItem.id, chunkIndex + 1);
if (chunkIndex === totalChunks - 1) {
// 合并文件
mergeFile(fileItem.id);
fileItem.status = 'completed';
startUpload(); // 开始下一个文件
}
} else {
console.error('上传失败:', xhr.responseText);
fileItem.status = 'error';
renderQueue();
}
}
};
try {
xhr.send(formData);
} catch (e) {
console.error('上传出错:', e);
fileItem.status = 'error';
renderQueue();
break;
}
}
}
// 暂停上传
function pauseUpload() {
if (currentUpload) {
currentUpload.status = 'paused';
renderQueue();
}
}
// 继续上传
function resumeUpload() {
if (currentUpload && currentUpload.status === 'paused') {
currentUpload.status = 'uploading';
uploadFile(currentUpload);
}
}
// 暂停单个项目
function pauseItem(fileId) {
var item = uploadQueue.find(item => item.id === fileId);
if (item) {
item.status = 'paused';
renderQueue();
}
}
// 继续单个项目
function resumeItem(fileId) {
var item = uploadQueue.find(item => item.id === fileId);
if (item && item.status === 'paused') {
item.status = 'uploading';
uploadFile(item);
}
}
// 取消单个项目
function cancelItem(fileId) {
var index = uploadQueue.findIndex(item => item.id === fileId);
if (index >= 0) {
// 通知后端取消上传
cancelUpload(fileId);
// 从队列移除
uploadQueue.splice(index, 1);
renderQueue();
}
}
// 合并文件
function mergeFile(fileId) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload/merge', false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('文件合并成功:', xhr.responseText);
localStorage.removeItem('resume_' + fileId);
}
};
xhr.send('fileId=' + encodeURIComponent(fileId) +
'&encryption=' + encodeURIComponent(document.getElementById('encryptionType').value));
}
// 取消上传
function cancelUpload(fileId) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload/cancel', false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('fileId=' + encodeURIComponent(fileId));
}
// 加载未完成的传输
function loadPendingTransfers() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/upload/pending', false);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var pendingFiles = JSON.parse(xhr.responseText);
pendingFiles.forEach(function(fileInfo) {
uploadQueue.push({
id: fileInfo.fileId,
name: fileInfo.fileName,
path: fileInfo.filePath,
size: fileInfo.fileSize,
progress: fileInfo.progress,
status: 'paused'
});
});
renderQueue();
}
};
xhr.send();
}
// 生成文件ID
function generateFileId(file) {
return file.name + '_' + file.size + '_' + file.lastModified;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 初始化
window.onload = init;
前端关键功能说明
1. 兼容IE9的黑魔法
- 使用条件注释只对IE9及以下浏览器加载polyfill
- json2.js解决IE9没有JSON对象的问题
- promise-polyfill解决IE9不支持Promise的问题
2. 文件夹上传核心代码
// 文件选择元素添加webkitdirectory和directory属性
// 处理文件时保留路径信息
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var fileItem = {
name: file.name,
path: file.webkitRelativePath || '', // 保留相对路径
size: file.size
// ...
};
uploadQueue.push(fileItem);
}
}
3. 断点续传实现
// 上传前检查本地是否有断点记录
var resumeChunk = localStorage.getItem('resume_' + fileItem.id) || 0;
// 上传成功后保存断点
localStorage.setItem('resume_' + fileItem.id, chunkIndex + 1);
// 文件合并成功后清理断点
localStorage.removeItem('resume_' + fileItem.id);
4. 加密传输(伪实现)
// 实际项目中应该使用Web Crypto API或相应库
formData.append('encryption', document.getElementById('encryptionType').value);
formData.append('encryptionKey', document.getElementById('encryptionKey').value);
如何与后端对接
需要后端提供的API接口
-
分块上传接口
POST /api/upload/chunk 参数: - fileId: 文件唯一ID - chunkIndex: 当前分块索引 - totalChunks: 总分块数 - fileName: 文件名 - filePath: 文件相对路径(文件夹结构) - fileSize: 文件大小 - chunkData: 分块数据 - encryption: 加密算法 - encryptionKey: 加密密钥 -
合并文件接口
POST /api/upload/merge 参数: - fileId: 文件唯一ID - encryption: 加密算法 -
取消上传接口
POST /api/upload/cancel 参数: - fileId: 文件唯一ID -
获取未完成任务接口
GET /api/upload/pending 返回: [ { fileId: string, fileName: string, filePath: string, fileSize: number, progress: number } ]
部署注意事项
-
IE9兼容性:
- 确保服务器正确设置
X-UA-Compatible头 - 添加MIME类型
.json application/json
- 确保服务器正确设置
-
大文件上传:
- 配置Nginx/Apache的上传大小限制
- 设置PHP的
upload_max_filesize和post_max_size
-
断点续传:
- 确保localStorage可用(IE8+支持)
- 对于隐私模式,需要降级使用cookie存储
-
加密传输:
- 实际项目中应该使用HTTPS
- 前端加密应该使用Web Crypto API或相应polyfill
最后吐槽
-
100块预算还要兼容IE9?甲方是不是对程序员有什么误解?
-
20G文件上传?建议先问问甲方他们服务器硬盘够不够大
-
7x24小时免费技术支持?我连7x24小时睡觉都保证不了…
-
要源代码?要文档?要一条龙服务?100块连个外卖都点不了好吗!
-
加群374992201领红包?兄弟,有这功夫不如多接几个项目…
不过既然你都看到这里了,代码拿去用吧,记得请我喝奶茶(至少得是喜茶级别的)!
将组件复制到项目中
示例中已经包含此目录
引入组件

配置接口地址
接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
处理事件

启动测试

启动成功

效果

数据库

效果预览
文件上传

文件刷新续传
支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件夹上传
支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
下载示例
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)