大文件上传是前端开发中常见的需求之一,特别是在需要处理较大的Excel表格数据、高清图片、视频或其他大型文件时。优化大文件上传不仅可以提升用户体验,还能有效减轻服务器负担。本文将深入探讨大文件上传的几种常见优化技术,包括文件切片与并发上传、断点续传、后台处理优化、安全性考虑和用户体验优化。
一、前言
在现代Web应用中,用户上传大文件已成为常见需求。然而,直接上传大文件会面临诸多挑战,例如网络不稳定导致上传中断、长时间上传导致用户体验差、服务器压力大等。因此,优化大文件上传性能显得尤为重要。
二、优化方案
1. 文件切片与并发上传
1.1 文件切片原理
文件切片(Chunking)是将大文件分成若干小片段,每个片段独立上传的方法。这样做可以有效减少单次上传的数据量,降低上传失败的概率。
这种方法可以提高上传效率和稳定性,并且支持断点续传。
1.2 实现步骤
- 前端切片:利用
Blob
对象的slice
方法将文件切片。 - 并发上传:使用
Promise.all
实现多个切片并发上传。 - 合并请求:上传完成后,通知服务器合并这些切片。
2. 断点续传
断点续传(Resumable Uploads)可以在上传过程中断(如网络故障、页面关闭等)时,从断点继续上传,避免重新上传整个文件。这通常通过记录已上传的分片索引来实现。
2.1 实现步骤
- 前端记录进度:使用
localStorage
记录已上传的切片信息。这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失 - 断点续传:上传时检查哪些切片未上传,继续上传未完成的部分。
3. 秒传功能
- 在服务端已经存在了上传的资源时,通过文件hash值快速判断文件是否存在,从而避免重复上传,节省时间和流量。
已经上传过的文件,并且在后端已经拼接完成,如果再次上传的话后端不做处理,直接返回拼接好的文件的信息即可,y一般主要后端实现
4. 基于WebWorker的并行处理
- 使用WebWorker来并行计算文件的分片hash值,可以显著提高大文件处理的速度。
WebWorker 实际上是运行在浏览器后台的一个单独的线程,因此可以执行一些耗时的操作而不会阻塞主线程。WebWorker 通过与主线程之间传递消息实现通信,这种通信是双向的。WebWorker不能直接访问 DOM,也不能使用像 window 对象这样的浏览器接口对象,但可以使用一些WebWorker 标准接口和 Navigator 对象的部分属性和方法。
主线程
主线程创建 worker 实例,向子线程通过 postMessage 发送消息,通过 onmessage 监听子线程返回的数据。
const myWorker = new Worker('./worker.js')
// 监听子线程返回的数据
myWorker.onmessage = function (e) {
console.log('Fibonacci result:', e.data)
}
// 向子线程发送消息
myWorker.postMessage(40) // 请求计算斐波那契数列的第40项
5. 压缩传输数据
- 对传输的数据进行压缩处理,减少传输时间和带宽消耗
6. 分布式存储
- 使用分布式存储系统,提高文件存储和访问的性能和可扩展性,减轻单个服务器的负载压力。
7. 后台处理优化
- 分片接收与合并:服务器需要支持接收分片请求,并在所有分片上传完成后合并文件。可以利用中间件或服务端程序语言实现这一逻辑。
8. 安全性考虑
- 文件类型校验:在前端和后端都应对文件类型进行校验,确保上传的文件类型符合预期。
- 文件大小限制:限制单个文件和总上传文件的大小,防止恶意用户上传过大的文件造成服务器压力。
9. 用户体验优化
- 进度显示:通过显示上传进度条,让用户了解上传进度,提升用户体验。
- 网络波动处理:考虑到用户可能在网络不稳定的环境中上传文件,可以增加失败重试机制。
完整实例
后端代码(Node.js + Express)
安装依赖
npm init -y
npm install express multer fs
创建服务器文件(server.js)
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(bodyParser.json());
// 路由:处理文件切片上传
app.post('/upload', upload.single('chunk'), (req, res) => {
const { index, fileName } = req.body;
const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
fs.renameSync(req.file.path, chunkPath);
res.status(200).send('Chunk uploaded');
});
// 路由:合并切片
app.post('/merge', (req, res) => {
const { totalChunks, fileName } = req.body;
const filePath = path.join(__dirname, 'uploads', fileName);
const writeStream = fs.createWriteStream(filePath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${i}`);
const data = fs.readFileSync(chunkPath);
writeStream.write(data);
fs.unlinkSync(chunkPath);
}
writeStream.end();
res.status(200).send('File merged');
});
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
前端代码(index.html + script.js)
1.创建HTML文件(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大文件上传</title>
</head>
<body>
<input type="file" id="fileInput">
<progress id="progressBar" value="0" max="100"></progress>
<button onclick="uploadFile()">上传文件</button>
<script src="script.js"></script>
</body>
</html>
2.创建JavaScript文件(script.js)
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');
const chunkSize = 5 * 1024 * 1024; // 5MB 每次最大切片的长度
// 每个切片要发送的ajax
const uploadChunk = async (chunk, index, fileName) => {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('fileName', fileName);
// 发送切片上传请求
await fetch('/upload', {
method: 'POST',
body: formData
});
updateProgressBar(index);
};
const updateProgressBar = (index) => {
const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
if (!uploadedChunks.includes(index)) {
uploadedChunks.push(index);
progressBar.value = (uploadedChunks.length / totalChunks) * 100;
localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
}
};
// 上传文件按钮事件
const uploadFile = async () => {
const file = fileInput.files[0];
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
const promises = [];
for (let i = 0; i < totalChunks; i++) {
if (!uploadedChunks.includes(i)) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
promises.push(uploadChunk(chunk, i, file.name));
}
}
// 多个切片并发上传
await Promise.all(promises);
await fetch('/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ totalChunks, fileName: file.name })
});
localStorage.removeItem('uploadedChunks');
alert('文件上传成功');
};
启动后端服务器
3.在浏览器中打开前端页面
将index.html
文件在浏览器中打开,选择文件并点击“上传文件”按钮即可看到文件上传进度。
node server.js
三、插件
目前成熟的大文件上传方案 目前社区已经存在一些成熟的大文件上传解决方案,也许并不需要我们手动去实现一个简陋的大文件上传库,但是了解其原理还是十分有必要的。
推荐的前端vue组件:vue-simple-uploader,支持vue2,vue3
vue-simple-uploader是基于simple-Uploader.js封装的大文件上传组件,具有以下优点:
- 支持单文件、多文件、文件夹上传;支持拖拽文件、文件夹上传
- 可暂停、继续上传
- 错误处理
- 支持“秒传”,通过文件判断服务端是否已存在从而实现“秒传”
- 分块上传
- 支持进度、预估剩余时间、出错自动重试、重传等操作
安装与配置
npm install vue-simple-uploader --save
然后在你的main.js
中引入并使用它:
import Vue from 'vue';
import uploader from 'vue-simple-uploader';
Vue.use(uploader);
接下来,配置上传选项,这些选项可以根据你的后端接口和业务需求进行调整:
options: {
target: ' http://localhost:8080', // SpringBoot后台接收文件夹数据的接口
simultaneousUploads: 10, // 支持同时上传数量
autoStart: false, // 自动上传
panelShow: false,
allowDuplicateUploads: false, // 上传过得文件不可以再上传
testChunks: false, // 是否分片-不分片
chunkSize: '102400000000', // 块大小
// query参数是带有数据的post的额外参数,policy、OSSAccessKeyId和signature是获取到的后端签名返回,
query: (file) => {
return {
name: file.name,
key: file.key,
policy,
OSSAccessKeyId: accessId,
signature,
success_action_status: 200, // success_action_status需设置为 200
};
},
}
常用方法与事件
vue-simple-uploader提供了多种方法和事件,以便于开发者根据需要进行自定义处理:
- assignBrowse:将非组件按钮绑定为上传按钮。
- getSize:获取上传文件的总大小。
- progress:获取上传进度。
- addFile:手动添加文件到上传队列。
事件处理包括但不限于:
- fileAdded:文件添加到上传队列时触发。
- filesAdded:多文件添加时触发。
- fileSuccess:文件上传成功时触发。
- complete:所有文件上传完成时触发。
- fileError:文件上传失败时触发。
代码实现
以下是vue-simple-uploader
组件的一个基本使用示例,包括组件声明、事件绑定和样式配置:
<template>
<!-- 定义Uploader组件 -->
<uploader
:key="uploader_key" <!-- 使用key确保组件在数据更新时重新渲染 -->
:options="options" <!-- 绑定配置项 -->
class="uploader-example" <!-- 添加自定义类名 -->
@file-added="onFileAdded" <!-- 文件添加时触发的事件 -->
@file-success="onFileSuccess" <!-- 文件上传成功时触发的事件 -->
@upload-start="uploadStr" <!-- 开始上传时触发的事件 -->
@complete="uploadEnd" <!-- 所有文件上传完成时触发的事件 -->
@file-error="fileError" <!-- 文件上传失败时触发的事件 -->
>
<!-- 定义不支持上传的提示 -->
<uploader-unsupport></uploader-unsupport>
<!-- 定义拖拽区域 -->
<uploader-drop>
<!-- 定义上传按钮,使用Element UI的按钮组件 -->
<el-button class="uploaders-btn">
<uploader-btn class="btn" :directory="true"> <!-- 设置为目录上传 -->
<el-icon><Notification /></el-icon> <!-- 使用Element UI的图标组件 -->
<span>上传文件夹</span> <!-- 按钮文本 -->
</uploader-btn>
</el-button>
</uploader-drop>
</uploader>
</template>
<script>
import md5 from "js-md5";
export default {
data() {
return {
// 用于刷新组件的key,每次上传时更改其值以刷新组件状态
uploader_key: new Date().getTime(),
// 配置项,根据后端接口和业务需求进行配置
options: {
//目标上传 URL,默认POST
target: "/api/file/uploadFile", // 后端接收数据的接口
//上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
ileParameterName: 'upfile',
//失败后最多自动重试上传次数
maxChunkRetries: 3,
query: (file, res, status) => {
// 返回上传所需的额外参数
return {
filePath: "",
identifier: md5(file.uniqueIdentifier),
parentUserFileId: this.firstId,
sourceMenuId: this.findId,
uuid: this.uuid,
};
},
headers: {
"Blade-Auth": "bearer " + getToken(), // 认证信息
},
testChunks: true, // 不分片上传
//分块大小(单位:字节)
chunkSize: '2048000',
},
fileStatusText: {
success: '上传成功',
error: '上传失败',
uploading: '上传中',
paused: '暂停',
waiting: '等待上传'
}
};
},
created() {
// 组件创建时初始化options
this.options = {
// ... 具体配置
};
},
methods: {
// 文件添加到上传队列时的处理函数
onFileAdded(file) {
console.log("文件添加到队列:", file);
// 每次添加文件时生成新的uuid
this.uuid = new Date().getTime();
},
// 文件上传成功时的处理函数
onFileSuccess(rootFile, file, response, chunk) {
console.log("文件上传成功:", file, response);
// 根据服务器返回的response处理业务逻辑
},
// 文件上传失败时的处理函数
fileError(rootFile, file, response, chunk) {
console.error("文件上传失败:", file, response);
// 显示错误信息
this.$message.error("文件夹上传失败");
},
// 开始上传时的处理函数
uploadStr() {
this.loadingFile = true; // 设置加载状态
},
// 所有文件上传完成时的处理函数
uploadEnd() {
this.loadingFile = false; // 重置加载状态
},
},
};
</script>
<style lang="scss" scoped>
/* 自定义样式 */
.uploader-example {
.uploaders-btn {
/* 按钮样式 */
}
.btn {
/* 上传按钮内的图标和文本样式 */
}
}
</style>