一、多文件上传功能的实现
1.准备工作
1.找到我们需要的上传模板,这里的drag为可拖拽,multiple为可多文件
2.根据需求做一些修改:
action="" :auto-upload="false"这里关闭自动上传,后续我们通过点击按钮来上传:file-list="fileList" :show-file-list="false"这里关闭了文件列表的默认显示,后续我们用table来完整显示文件的相关信息以及添加一些控制按钮:on-change="handleFileChange"这里我们使用了一个文件状态改变时的钩子,用于读取fileList
2.Task(任务①):显示文件大小,文件数量统计
Action(行动):
- 1.:on-change="handleFileChange"
- 2.handleFileChange(file, fileList) {console.log(file, fileList)}
- 3.看一眼
handleFileChange(file, fileList) {
console.log(file, fileList);
this.cancelTokens={}
this.progress={}
this.fileList=[]
fileList.forEach((file) => {
file.uploading = false;
file.progress = 0;
this.$set(this.progress, file.uid, 0);
});
this.fileList = fileList;
console.log(this.fileList);
},
通过观察,这里的on-change钩子函数,当文件状态发生改变时会触发它,我们可以通过这个钩子函数获取两个返回的参数
Result(结果):
- 1.file是fileList最后一个文件的信息
- 2.fileList是一个数组,包含所有文件的file信息
- 3.file:{name,percentage,raw[uid,lastModified,lastModifiedDate],size,type,webkitRelativePath}
raw里面包含了文件的详细信息,包括了它的创建时间,名称以及文件类型,我们要上传的就是这个玩意
这里可以注意到一些我们后续需要使用的一些属性
1.progress为上传的进度
2.size为文件大小,单位:字节
3.uid为唯一id
4.uploading上传
那么我们现在可以渲染table表格和一些相关的信息了
<el-table :data="fileList" border style="margin-top: 20px">
<el-table-column prop="name" label="文件名" />
<el-table-column prop="size" label="大小 (KB)" width="100">
<template #default="scope">
{{ (scope.row.size / 1024).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="scope">
<span v-if="progress[scope.row.uid] === 100">上传成功</span>
<span v-else-if="progress[scope.row.uid] === 0">待上传</span>
<span v-else>上传中</span>
</template>
</el-table-column>
<el-table-column label="进度" width="150">
<template #default="scope">
<el-progress :text-inside="true" :stroke-width="24" :percentage="progress[scope.row.uid]" status="success">
</el-progress>
<!-- <el-progress
:percentage="scope.row.progress"
v-if="scope.row.progress <= 100 && scope.row.progress >= 0"
/> -->
<!-- v-if="scope.row.uploading" -->
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button
size="small"
type="info"
@click="cancelSingleUpload(scope.row)"
>
取消
</el-button>
<el-button
size="small"
type="danger"
@click="removeFile(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
3.Task(任务②):使用 axios 实现并发文件上传,并对上传进度进行监控
Action(行动):
- 1.:auto-upload="false"
- 2.使用 axios 配合 Promise.allSettled 实现并发上传文件。
- 3.:on-progress="onUploadProgress"
注意:我们在这次行动中做了两个变化,属于行动中时产生的一些优化
- 1.当我们要使用Promise.allSettled并发上传时,我们可以提前封装一个单文件上传的函数并且把请求return出去,这样我们便可以轻松的得到一个promise对象数组来配合使用
- 2.这里我选择使用AJAX在进行文件上传时的一个回调函数onUploadProgress用于监听上传进度,e.loaded为上传字节数,e.total为文件总字节数
- 3.因为要独立每一个文件上传的进度,这里我们在data里面配置了一个数据progress,以文件的id为属性名,进度为属性值,进行传入。其实这里不难发现我们后续的取消令牌也是用的此方式
uploadFile(file, uid) {
const formData = new FormData();
formData.append("file", file);
const source = axios.CancelToken.source();
this.$set(this.cancelTokens,uid,source)
console.log('取消令牌',this.cancelTokens);
// this.cancelTokens[uid] = source;
// formData.append("type", "contract");
return axios.post(
"https://jsonplaceholder.typicode.com/posts/",
formData,
{
cancelToken: source.token,
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
file.progress = percentCompleted;
this.$set(this.progress, uid, percentCompleted)
console.log(this.progress);
console.log(`Upload progress: ${file.progress}%`);
},
}
);
// .then((response) => {
// file.uploading = false;
// // 处理上传成功的响应
// })
},
Result(结果):
- 1.开始上传按钮应写在el-upload标签外部
- 2.封装了一个单文件上传函数,遍历fileList调用上传函数获得promise对象数组,使用Promise.allSettled并发上传
- 3.单文件上传函数中上传进度监控onUploadProgress
- 4.解决进度条视图不响应问题,使用了this.set()方法
submitUpload() {
const uploadList = this.fileList.map((item) => {
return this.uploadFile(item.raw, item.uid);
});
Promise.allSettled(uploadList)
.then((res) => {
console.log("res:", res);
})
.catch((error) => {
console.error("上传失败:", error);
});
}
4.Task(任务③):实现取消单个文件上传和清除所有任务的功能
Action(行动):
- 1.创建数据cancelTokens: {},每个id绑一个自己的取消令牌源对象
- 2.将请求与取消令牌关联起来
- 3.通过id来取消请求,或者遍历Object.values()全部取消
重点理解:
- axios.CancelToken.source()创建一个取消令牌(Cancel Token)的源对象,返回token 和 cancel 两个属性的对象
- ①将取消令牌token传递给 Axios 请求的 cancelToken 配置选项//token是用来绑定的
- ②source.cancel('请求被手动取消')//cancel是用来取消请求的
Result(结果):
- 1.const source = axios.CancelToken.source()
- 2.cancelToken: source.token
- 3.source.cancel()
- 总结:我们在单文件上传中已经完成了前两步,我们的source以键值对的方式(key为文件id)存在了cancelTokens中,那我们只需要拿到文件的id就可以执行第三步取消请求了!
cancelAllUploads() {
Object.values(this.cancelTokens).forEach((source) => {
source.cancel("全部上传已取消");
});
Message.success("全部上传已取消");
this.cancelTokens = {};
},
cancelSingleUpload(file) {
const source = this.cancelTokens[file.uid];
if (source) {
source.cancel("单个文件上传已取消");
delete this.cancelTokens[file.uid];
Message.success("单个文件上传已取消");
}
},
二、超大文件上传优化方式
1.切片上传
原理
将大文件分割成多个较小的文件块(切片),然后分别上传这些切片,最后在服务器端将这些切片按顺序合并成完整的文件。
优势
- 降低上传失败风险:如果在上传过程中某个切片失败,只需重新上传该切片,而不需要重新上传整个大文件。
- 提高上传效率:可以并行上传多个切片,充分利用网络带宽,加快上传速度。
实现步骤
-
客户端:
- 文件切片:使用 JavaScript 的
File.prototype.slice方法将文件分割成多个切片。 - 并行上传:使用多线程或异步请求同时上传多个切片。可以使用
XMLHttpRequest或fetchAPI 发送切片数据。 - 记录切片信息:为每个切片分配唯一的标识,记录切片的序号、文件总切片数等信息,以便服务器端合并。
- 文件切片:使用 JavaScript 的
// 获取文件输入元素 const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const chunkSize = 1024 * 1024; // 每个切片大小为 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
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('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
try { await fetch('your_upload_url', { method: 'POST', body: formData }); }
catch (error) { console.error('切片上传失败:', error); } } });
2. 断点上传
原理
在文件上传过程中,如果出现网络中断、页面关闭等异常情况,下次上传时可以从上次中断的位置继续上传,而不需要重新开始。
优势
- 提高用户体验:避免用户因意外情况而重新上传整个文件,节省时间和流量。
- 增强上传稳定性:即使在不稳定的网络环境下,也能保证文件最终可以完整上传。
实现步骤
-
客户端:
- 记录上传进度:在每次上传切片成功后,记录已上传的切片序号或字节数。
- 检查断点:在重新上传文件时,检查是否存在上次上传的断点信息。如果存在,则从断点位置开始继续上传。
// 获取文件输入元素 const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const chunkSize = 1024 * 1024; // 每个切片大小为 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
// 获取上次上传的断点信息
let startChunk = localStorage.getItem('uploadChunkIndex') || 0;
for (let i = startChunk; 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('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
try { await fetch('your_upload_url', { method: 'POST', body: formData });
// 更新断点信息
localStorage.setItem('uploadChunkIndex', i + 1); }
catch (error) { console.error('切片上传失败:', error); break; } } });
3. 并行上传
原理
同时上传多个文件切片,充分利用网络带宽,提高上传速度。
优势
- 加快上传速度:在网络条件允许的情况下,并行上传可以显著缩短文件上传的时间。
实现步骤
-
客户端:
- 切片划分:将大文件分割成多个切片。
- 并行请求:使用多线程或异步请求同时上传多个切片。可以使用
Promise.all或Promise.allSettled来管理并行请求。
// 获取文件输入元素
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0]; const chunkSize = 1024 * 1024;
// 每个切片大小为 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadPromises = [];
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('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
const uploadPromise = fetch('your_upload_url', { method: 'POST', body: formData });
uploadPromises.push(uploadPromise); }
try { await Promise.all(uploadPromises);
console.log('文件上传完成'); }
catch (error) { console.error('文件上传失败:', error); } });
4. 压缩文件
原理
在上传文件之前,对文件进行压缩处理,减小文件的大小,从而减少上传所需的时间和带宽。
优势
- 节省带宽和时间:压缩后的文件体积更小,上传速度更快。
实现步骤
-
客户端:
- 选择压缩算法:常见的压缩算法有 ZIP、GZIP 等。可以使用 JavaScript 库(如
jszip)进行文件压缩。 - 压缩文件:将大文件压缩成压缩包,然后上传压缩包。
- 选择压缩算法:常见的压缩算法有 ZIP、GZIP 等。可以使用 JavaScript 库(如
import JSZip from 'jszip'; // 获取文件输入元素
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const zip = new JSZip();
zip.file(file.name, file);
const compressedBlob = await zip.generateAsync({ type: 'blob' });
const formData = new FormData();
formData.append('compressedFile', compressedBlob);
try { await fetch('your_upload_url', { method: 'POST', body: formData });
console.log('压缩文件上传完成'); }
catch (error) { console.error('压缩文件上传失败:', error); } });
5. 预上传检查
原理
在开始上传文件之前,先对文件进行一些检查,如文件大小、文件类型、文件完整性等,避免上传不符合要求的文件。
优势
- 节省资源:避免上传无效或不符合要求的文件,减少服务器和网络资源的浪费。
实现步骤
-
客户端:
- 文件大小检查:检查文件的大小是否超过服务器允许的最大限制。
- 文件类型检查:检查文件的类型是否符合要求,如只允许上传图片、文档等特定类型的文件。
- 文件完整性检查:可以使用哈希算法(如 MD5、SHA-1 等)计算文件的哈希值,然后将哈希值发送到服务器进行验证。
// 获取文件输入元素
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0]; const maxSize = 1024 * 1024 * 10; // 最大文件大小为 10MB
const allowedTypes = ['image/jpeg', 'image/png'];
if (file.size > maxSize) { console.error('文件大小超过限制'); return; }
if (!allowedTypes.includes(file.type)) { console.error('不支持的文件类型'); return; }
// 计算文件哈希值(示例中使用伪代码)
const hash = calculateFileHash(file); // 发送检查请求
fetch('your_check_url', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ size: file.size, type: file.type, hash: hash }) })
.then(response => response.json())
.then(data => { if (data.valid) { // 文件检查通过,开始上传 uploadFile(file); }
else { console.error('文件检查未通过:', data.message); } })
.catch(error => { console.error('文件检查请求失败:', error); }); });
function calculateFileHash(file) { // 实际实现中需要使用具体的哈希算法库 return 'dummy_hash'; }
function uploadFile(file) { // 实现文件上传逻辑 }
HTTP/2 特性及其对超大文件上传的优化作用
1. 二进制分帧
- 原理:HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式编码。每个帧都有一个帧头,包含了该帧的元信息,如所属的流编号等。消息由一个或多个帧组成,流则是多个消息的双向传输序列。
- 对超大文件上传的优化:在上传超大文件时,文件被分割成多个二进制帧进行传输。这些帧可以独立发送和接收,服务器可以根据帧的优先级和可用资源灵活处理,避免了 HTTP/1.x 中请求排队和阻塞的问题,提高了上传效率。
2. 多路复用
- 原理:允许在一个 TCP 连接上同时进行多个流的传输,各个流之间相互独立、互不干扰。客户端和服务器可以同时发送和接收多个请求和响应,无需等待前一个请求处理完成。
- 对超大文件上传的优化:在超大文件上传过程中,可将文件切片上传的多个请求复用在同一个 TCP 连接上。例如,客户端可以同时发送多个文件切片的上传请求,服务器也能同时处理并返回响应,大大提高了并发性能,减少了连接建立和关闭的开销,加快了上传速度。
3. 头部压缩
- 原理:HTTP/2 使用 HPACK 算法对请求和响应的头部进行压缩。该算法通过建立静态和动态表来存储已经出现过的头部字段及其值,当再次出现相同的头部字段时,只需引用表中的索引,从而减少了头部数据的传输量。
- 对超大文件上传的优化:在上传超大文件时,多次请求的头部信息往往有很多重复部分。通过头部压缩,可显著减少每个请求的头部数据大小,降低了网络带宽的占用,提高了数据传输效率。
4. 优先级与流量控制
-
原理:
- 优先级:客户端可以为每个流指定优先级,服务器根据优先级来分配资源,优先处理高优先级的流。
- 流量控制:HTTP/2 提供了基于流和连接的流量控制机制,发送方需要等待接收方的窗口更新通知才能继续发送数据,防止接收方缓冲区溢出。
-
对超大文件上传的优化:在超大文件上传时,可以为关键的控制信息或重要的文件切片设置较高的优先级,确保这些数据能优先处理。同时,流量控制机制可以根据网络状况和接收方的处理能力,合理调整上传速度,避免网络拥塞和数据丢失,保证上传的稳定性。
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
const chunkSize = 1024 * 1024; // 每个切片大小为 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
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('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
try { const response = await fetch('your_upload_url', { method: 'POST', body: formData });
if (response.ok) { console.log(`切片 ${i} 上传成功`); }
else { console.error(`切片 ${i} 上传失败`); } }
catch (error) { console.error('切片上传出错:', error); } } });
const express = require('express');
const app = express();
const fs = require('fs');
const path = require('path');
app.use(express.raw({ type: '*/*' }));
app.post('/your_upload_url', (req, res) => {
const chunkIndex = parseInt(req.query.chunkIndex);
const totalChunks = parseInt(req.query.totalChunks);
const chunk = req.body;
const tempDir = path.join(__dirname, 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir); }
const chunkPath = path.join(tempDir, `chunk_${chunkIndex}`);
fs.writeFileSync(chunkPath, chunk);
if (chunkIndex === totalChunks - 1) {
// 所有切片上传完成,合并切片
const mergedFilePath = path.join(__dirname, 'merged_file');
const writeStream = fs.createWriteStream(mergedFilePath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(tempDir, `chunk_${i}`);
const readStream = fs.createReadStream(chunkPath);
readStream.pipe(writeStream, { end: false });
readStream.on('end', () => { fs.unlinkSync(chunkPath); }); }
writeStream.on('finish', () => { console.log('文件合并完成');
res.status(200).send('文件上传并合并成功'); }); }
else { res.status(200).send('切片上传成功'); } });
const port = 3000;
app.listen(port, () => { console.log(`服务器运行在端口 ${port}`); });
三、超大文件数据接收优化方式
场景:你向后端发了个数据请求,返回了1000个G的内容,并且此刻后端人员在厨房手持菜刀(明显打不过),怎么办?很急!
1. 流式处理数据
流式处理允许前端在数据传输过程中就开始处理数据,而不是等待整个响应下载完成,这样可以减少内存占用。
async function streamData() {
const response = await fetch('your_api_endpoint');
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break; // 这里可以对每一块数据进行处理,例如解析、存储等
console.log('Received chunk:', value); } }
streamData();
2. 利用 IndexedDB 存储数据
由于前端内存有限,无法一次性容纳 1000GB 数据,而 IndexedDB 是一种基于浏览器的数据库,可以存储大量数据。可以将接收到的数据分块存储到 IndexedDB 中。
async function saveToIndexedDB(dataChunk, chunkIndex) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('LargeDataDB', 1);
request.onupgradeneeded = function(event) {
const db = event.target.result;
const objectStore = db.createObjectStore('chunks', { keyPath: 'id' }); };
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(['chunks'], 'readwrite');
const objectStore = transaction.objectStore('chunks');
const addRequest = objectStore.add({ id: chunkIndex, data: dataChunk });
addRequest.onsuccess = function() { resolve(); };
addRequest.onerror = function() { reject(new Error('Failed to save chunk to IndexedDB')); }; };
request.onerror = function() { reject(new Error('Failed to open IndexedDB')); }; }); }
async function receiveAndStoreData() {
const response = await fetch('your_api_endpoint');
const reader = response.body.getReader();
let chunkIndex = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
await saveToIndexedDB(value, chunkIndex); chunkIndex++; } }
receiveAndStoreData();
3. 数据压缩处理(使用 pako 库进行 Gzip 解压缩)
如果服务器返回的数据可以压缩,前端可以在接收后进行解压缩。这样在传输过程中可以减少数据量,提高传输效率。不过前端解压缩也会消耗一定的 CPU 资源。
import pako from 'pako';
async function receiveAndDecompressData() {
const response = await fetch('your_api_endpoint');
const reader = response.body.getReader();
let buffer = [];
while (true) {
const { done, value } = await reader.read();
if (done) break; buffer.push(value); }
const concatenatedBuffer = new Uint8Array([].concat(...buffer));
const decompressedData = pako.inflate(concatenatedBuffer);
console.log('Decompressed data:', decompressedData); }
receiveAndDecompressData();