欢迎光临
我们一直在努力

vue2环境下如何编写大文件分片上传的完整源码?

大文件上传解决方案

各位同行大佬们好,作为一个在广东摸爬滚打多年的前端"老油条",最近接了个让我差点秃顶的项目——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文件上传?建议先问问甲方他们服务器硬盘够不够大

  • 7×24小时免费技术支持?我连7×24小时睡觉都保证不了…

  • 要源代码?要文档?要一条龙服务?100块连个外卖都点不了好吗!

  • 加群374992201领红包?兄弟,有这功夫不如多接几个项目…

  • 不过既然你都看到这里了,代码拿去用吧,记得请我喝奶茶(至少得是喜茶级别的)!

    将组件复制到项目中

    示例中已经包含此目录 image

    引入组件

    image

    配置接口地址

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

    处理事件

    image

    启动测试

    image

    启动成功

    image

    效果

    image

    数据库

    image

    效果预览

    文件上传

    文件上传

    文件刷新续传

    支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传 文件续传

    文件夹上传

    支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。 文件夹上传

    下载示例

    点击下载完整示例

    赞(0)
    未经允许不得转载:171主机测评 » vue2环境下如何编写大文件分片上传的完整源码?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址