使用场景
1. 电商平台
- 商家批量上传商品图片,如不同角度的商品展示图、规格图等。
- 在编辑商品时上传多张图片方便商品展示,提升用户体验。
2. 社交媒体和内容平台
- 用户上传多张照片、视频或文件分享个人动态或相册。
- 在图片分享和内容管理平台中,批量上传便于用户一次性上传多张图片,提升操作便捷性。
3. 内容管理系统(CMS)
- 站点管理者上传多媒体资源(图片、视频、文件等)以供网站使用。
- 批量上传可以帮助管理员高效地更新图片库和资源库。
4. 企业内部管理系统
- 批量上传员工证件照、身份证扫描件等,方便快速生成档案。
- 上传多个项目文件(如图纸、照片等)用于项目汇报、记录等需求。
5. 教育平台
- 教师上传多张课件图片、讲义或作业参考材料供学生参考。
- 学生上传多个作业文件,如笔记、照片、实验图片等。
6. 房地产、旅游等行业平台
2. 文件预处理
3. 上传进度管理
4. 错误处理与重试机制
具体的代码
第一点优化
- 房地产平台上传房源照片,如房间的各个角度、社区环境等。
- 旅游平台上传景点照片、活动照片等,展示更丰富的视觉内容。
难点和亮点
批量上传的目的是提高用户上传大量文件的效率,并通过适当的并发控制避免对服务器造成负担
1. 并发控制和性能优化
- 并发限制:批量上传如果没有控制上传并发数量,容易引发请求暴增,导致服务器负载过高,可能造成服务器响应慢甚至宕机。
- 文件大小限制:大图片文件的传输时间较长,会导致上传耗时过久,影响用户体验。需要对大文件进行压缩或分片处理。
- 图片压缩:在上传前对图片进行压缩,以减少上传时间和服务器存储空间。但压缩处理会占用前端资源,可能造成卡顿。
- 格式转换:不同的图片格式可能需要在上传前进行转换(如将HEIC转换为JPEG),以提高兼容性,这通常需要使用Web Worker来避免阻塞UI线程。
- 进度展示:用户体验中实时显示上传进度是非常重要的,特别是批量上传的场景。需要管理多个图片的进度,更新UI展示上传百分比或进度条。
- 断点续传:如果上传中断(如网络不稳定或中途关闭页面),需支持断点续传,以确保用户不会因一次失败重新上传所有文件。
- 失败重试:上传过程中可能遇到网络波动、请求失败、文件格式不符合等问题。批量上传需要提供合理的重试机制或失败提示,让用户选择是否重新上传。
- 错误回调:批量上传涉及多个文件时,每个文件可能会遇到不同的上传错误,需要有精细的错误回调来区分错误类型并进行相应处理。
- 并发限制:控制同时上传的图片数量,避免服务器压力过大。
- 文件大小限制:在上传前检查文件大小,超出限制则阻止上传。
<template> <div> <el-upload ref="uploadRef" :http-request="customHttpRequest" :on-change="handleFileChange" :before-upload="beforeUpload" :file-list="fileList" multiple :limit="10" :auto-upload="false" > <el-button type="primary">批量上传图片</el-button> </el-upload> <div> <el-button type="primary" @click="startUpload">开始上传</el-button> </div> </div> </template> <script setup> import { ref } from 'vue'; import { ElButton, ElUpload, ElMessage } from 'element-plus'; const fileList = ref([]); const maxConcurrentUploads = 3; // 最大并发上传数量 const maxSizeInMB = 2; // 文件大小限制,单位:MB let uploadQueue = []; let currentUploads = 0; const handleFileChange = (file, files) => { // 将文件加入到队列中 uploadQueue.push(file); }; // 文件大小限制检查 const beforeUpload = (file) => { const isUnderLimit = file.size / 1024 / 1024 < maxSizeInMB; if (!isUnderLimit) { ElMessage.error(`文件 ${file.name} 超出大小限制(最大 ${maxSizeInMB} MB)`); } return isUnderLimit; }; // 控制并发上传,限制同时上传数量 const customHttpRequest = (options) => { if (currentUploads >= maxConcurrentUploads) return; currentUploads++; const { file, onProgress, onSuccess, onError } = options; // 创建XMLHttpRequest并配置上传进度 const xhr = new XMLHttpRequest(); xhr.open('POST', options.action, true); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const progress = (event.loaded / event.total) * 100; onProgress({ percent: progress }); } }; xhr.onload = () => { currentUploads--; processQueue(); onSuccess(xhr.response); }; xhr.onerror = () => { currentUploads--; processQueue(); onError(xhr.response); }; const formData = new FormData(); formData.append('file', file); xhr.send(formData); }; // 处理队列,限制同时上传数量 const processQueue = () => { while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) { const file = uploadQueue.shift(); customHttpRequest({ action: 'https://your-upload-endpoint', file, onProgress: (event) => console.log('progress:', event.percent), onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`), onError: () => ElMessage.error(`文件 ${file.name} 上传失败`), }); } }; // 开始上传 const startUpload = () => { processQueue(); }; </script>
- 文件大小检查:
beforeUpload
函数会在文件加入队列前判断文件大小是否超过maxSizeInMB
限制,如果超出则阻止上传,并给出提示信息。 - 并发上传限制:
- 定义了
maxConcurrentUploads
限制同时上传的文件数量。 uploadQueue
用于保存待上传的文件队列,currentUploads
记录当前上传中的文件数量。customHttpRequest
是自定义的上传请求,在每次上传完毕后递减currentUploads
,并调用processQueue
继续处理队列中的文件。processQueue
函数会检查uploadQueue
并发起新的上传请求,确保不会超过并发上传限制
第二点优化
使用Web Worker
对图片文件在上传前进行压缩和格式转换。通过Web Worker
来处理这些耗时操作,避免主线程阻塞,提升用户体验。
创建一个Web Worker
文件 imageWorker.js
,用于处理图片的压缩和格式转换操作。使用Canvas
API实现图片压缩,并将图片格式转换为JPEG
或PNG
等常见格式。
// imageWorker.js
self.onmessage = async function (e) {
const { file, quality, targetFormat } = e.data;
const compressImage = async (file, quality, format) => {
return new Promise((resolve) => {
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
// 设置格式和质量,转换图片
canvas.toBlob(
(blob) => {
const compressedFile = new File([blob], file.name, {
type: `image/${format}`,
lastModified: Date.now(),
});
resolve(compressedFile);
},
`image/${format}`,
quality
);
};
});
};
// 调用压缩方法
const processedFile = await compressImage(file, quality, targetFormat);
self.postMessage(processedFile);
};
Vue 组件中使用 Web Worker 进行图片预处理
<template>
<div>
<el-upload
ref="uploadRef"
:http-request="customHttpRequest"
:on-change="handleFileChange"
:before-upload="beforeUpload"
:file-list="fileList"
multiple
:limit="10"
:auto-upload="false"
>
<el-button type="primary">批量上传图片</el-button>
</el-upload>
<div>
<el-button type="primary" @click="startUpload">开始上传</el-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
const fileList = ref([]);
const maxConcurrentUploads = 3; // 最大并发上传数
let uploadQueue = [];
let currentUploads = 0;
let worker;
const handleFileChange = (file, files) => {
uploadQueue.push(file);
};
// 初始化 Web Worker
onMounted(() => {
worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});
onUnmounted(() => {
if (worker) worker.terminate();
});
// 图片预处理,压缩和格式转换
const beforeUpload = (file) => {
return new Promise((resolve) => {
worker.postMessage({
file,
quality: 0.7, // 图片压缩质量,0到1之间
targetFormat: 'jpeg', // 目标格式,可以是 'jpeg' 或 'png'
});
// 监听 Web Worker 返回的压缩文件
worker.onmessage = (e) => {
const processedFile = e.data;
resolve(processedFile); // 返回压缩后的文件
};
});
};
// 自定义上传请求,限制并发数量
const customHttpRequest = (options) => {
if (currentUploads >= maxConcurrentUploads) return;
currentUploads++;
const { file, onProgress, onSuccess, onError } = options;
const xhr = new XMLHttpRequest();
xhr.open('POST', options.action, true);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const progress = (event.loaded / event.total) * 100;
onProgress({ percent: progress });
}
};
xhr.onload = () => {
currentUploads--;
processQueue();
onSuccess(xhr.response);
};
xhr.onerror = () => {
currentUploads--;
processQueue();
onError(xhr.response);
};
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
};
// 处理队列,限制同时上传数量
const processQueue = () => {
while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
const file = uploadQueue.shift();
customHttpRequest({
action: 'https://your-upload-endpoint',
file,
onProgress: (event) => console.log('progress:', event.percent),
onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
onError: () => ElMessage.error(`文件 ${file.name} 上传失败`),
});
}
};
// 开始上传
const startUpload = () => {
processQueue();
};
</script>
- Web Worker压缩和格式转换:
imageWorker.js
文件中,使用Canvas
API压缩图片并转换格式,通过postMessage
返回处理后的文件。 compressImage
函数将图片绘制到Canvas
上,并将其转换为指定的格式和质量。- Vue组件中使用 Web Worker:
- 在
beforeUpload
钩子中,图片会传入Web Worker
进行预处理(压缩和格式转换)。 worker.onmessage
监听预处理完成后的文件,并将其加入到上传队列。- 自定义上传和并发控制:通过
customHttpRequest
和processQueue
方法,控制同时上传的数量,确保不会超出maxConcurrentUploads
的限制。
第三点优化
实现上传进度管理的UI展示、断点续传和多文件进度管理,我们可以做以下几项优化:
- 上传进度展示:为每张图片添加进度条,实时显示上传进度。
- 断点续传:对已上传的数据做断点标记,当上传中断时可以从中断的部分继续上传。
- 多文件进度管理:管理每个文件的上传进度状态,并在UI上展示。
<template>
<div>
<el-upload
ref="uploadRef"
:http-request="customHttpRequest"
:on-change="handleFileChange"
:before-upload="beforeUpload"
:file-list="fileList"
multiple
:auto-upload="false"
>
<el-button type="primary">批量上传图片</el-button>
</el-upload>
<div>
<el-button type="primary" @click="startUpload">开始上传</el-button>
</div>
<div v-for="(file, index) in uploadStatus" :key="file.uid" class="upload-item">
<span>{{ file.name }}</span>
<el-progress :percentage="file.progress" v-if="file.status === 'uploading'" />
<span v-if="file.status === 'completed'">上传完成</span>
<span v-if="file.status === 'failed'">上传失败</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
const fileList = ref([]);
const uploadStatus = ref([]);
const maxConcurrentUploads = 3; // 最大并发上传数
let uploadQueue = [];
let currentUploads = 0;
let worker;
const handleFileChange = (file) => {
uploadQueue.push(file);
uploadStatus.value.push({
uid: file.uid,
name: file.name,
progress: 0,
status: 'pending',
});
};
// 文件预处理,压缩和格式转换
const beforeUpload = (file) => {
return new Promise((resolve) => {
worker.postMessage({
file,
quality: 0.7,
targetFormat: 'jpeg',
});
worker.onmessage = (e) => {
const processedFile = e.data;
resolve(processedFile);
};
});
};
// 自定义上传请求,限制并发数量,支持断点续传
const customHttpRequest = (options) => {
if (currentUploads >= maxConcurrentUploads) return;
currentUploads++;
const { file, onProgress, onSuccess, onError } = options;
const storedProgress = localStorage.getItem(`upload-progress-${file.uid}`) || 0;
let uploadedBytes = parseInt(storedProgress, 10);
// 上传进度更新
const updateProgress = (event) => {
const progress = ((uploadedBytes + event.loaded) / file.size) * 100;
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.progress = progress;
onProgress({ percent: progress });
localStorage.setItem(`upload-progress-${file.uid}`, uploadedBytes + event.loaded);
};
// 自定义分块上传实现断点续传
const chunkSize = 1024 * 1024; // 1MB的分块大小
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = Math.floor(uploadedBytes / chunkSize);
const uploadChunk = () => {
if (currentChunk >= totalChunks) {
localStorage.removeItem(`upload-progress-${file.uid}`);
currentUploads--;
processQueue();
onSuccess();
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.status = 'completed';
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('filename', file.name);
formData.append('chunkNumber', currentChunk);
formData.append('totalChunks', totalChunks);
const xhr = new XMLHttpRequest();
xhr.open('POST', options.action, true);
xhr.upload.onprogress = updateProgress;
xhr.onload = () => {
uploadedBytes += chunk.size;
currentChunk++;
uploadChunk();
};
xhr.onerror = () => {
currentUploads--;
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.status = 'failed';
onError();
};
xhr.send(formData);
};
uploadChunk();
};
// 处理队列,限制同时上传数量
const processQueue = () => {
while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
const file = uploadQueue.shift();
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.status = 'uploading';
customHttpRequest({
action: 'https://your-upload-endpoint',
file,
onProgress: (event) => console.log('progress:', event.percent),
onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
onError: () => ElMessage.error(`文件 ${file.name} 上传失败`),
});
}
};
// 开始上传
const startUpload = () => {
processQueue();
};
// 初始化 Web Worker
onMounted(() => {
worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});
onUnmounted(() => {
if (worker) worker.terminate();
});
</script>
<style>
.upload-item {
display: flex;
align-items: center;
margin-top: 10px;
}
</style>
- 断点续传:我们将每个文件按1MB大小分块上传。
localStorage
中记录上传进度,当网络中断或页面关闭后,可以从最后一次成功上传的分块继续。 customHttpRequest
方法根据存储的进度决定从哪个分块开始上传。- 每完成一块上传,更新
uploadedBytes
并保存到localStorage
中,以便下次继续上传。 - 上传进度管理:
uploadStatus
数组用于跟踪每个文件的上传状态和进度。- 每次分块上传进度通过
onProgress
事件更新。 - 通过
el-progress
展示每个文件的上传进度,更新UI。 - 错误处理:
- 如果某个分块上传失败,标记为
failed
并展示在UI中,用户可以选择手动重试。
第4点优化
- 错误提示:为每个文件记录错误信息并展示在UI上。
- 错误分类和重试:实现精细化的错误回调,通过
retryCount
控制每个文件的重试次数。如果超过最大重试次数,则提供手动重试选项。 - 重试按钮:在上传失败的文件上显示“重试”按钮,让用户在手动点击时可以重新上传该文件
<template>
<div>
<el-upload
ref="uploadRef"
:http-request="customHttpRequest"
:on-change="handleFileChange"
:before-upload="beforeUpload"
:file-list="fileList"
multiple
:auto-upload="false"
>
<el-button type="primary">批量上传图片</el-button>
</el-upload>
<div>
<el-button type="primary" @click="startUpload">开始上传</el-button>
</div>
<div v-for="(file, index) in uploadStatus" :key="file.uid" class="upload-item">
<span>{{ file.name }}</span>
<el-progress :percentage="file.progress" v-if="file.status === 'uploading'" />
<span v-if="file.status === 'completed'">上传完成</span>
<span v-if="file.status === 'failed'" class="error-message">
上传失败:{{ file.error }}
<el-button type="text" @click="retryUpload(file)">重试</el-button>
</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
const fileList = ref([]);
const uploadStatus = ref([]);
const maxConcurrentUploads = 3;
const maxRetries = 3; // 最大重试次数
let uploadQueue = [];
let currentUploads = 0;
let worker;
const handleFileChange = (file) => {
uploadQueue.push(file);
uploadStatus.value.push({
uid: file.uid,
name: file.name,
progress: 0,
status: 'pending',
error: null,
retryCount: 0,
});
};
const beforeUpload = (file) => {
return new Promise((resolve) => {
worker.postMessage({
file,
quality: 0.7,
targetFormat: 'jpeg',
});
worker.onmessage = (e) => {
const processedFile = e.data;
resolve(processedFile);
};
});
};
// 自定义上传请求,限制并发数量,支持断点续传和错误处理
const customHttpRequest = (options) => {
if (currentUploads >= maxConcurrentUploads) return;
currentUploads++;
const { file, onProgress, onSuccess, onError } = options;
const storedProgress = localStorage.getItem(`upload-progress-${file.uid}`) || 0;
let uploadedBytes = parseInt(storedProgress, 10);
// 上传进度更新
const updateProgress = (event) => {
const progress = ((uploadedBytes + event.loaded) / file.size) * 100;
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.progress = progress;
onProgress({ percent: progress });
localStorage.setItem(`upload-progress-${file.uid}`, uploadedBytes + event.loaded);
};
// 自定义分块上传实现断点续传
const chunkSize = 1024 * 1024; // 1MB的分块大小
const totalChunks = Math.ceil(file.size / chunkSize);
let currentChunk = Math.floor(uploadedBytes / chunkSize);
const uploadChunk = () => {
if (currentChunk >= totalChunks) {
localStorage.removeItem(`upload-progress-${file.uid}`);
currentUploads--;
processQueue();
onSuccess();
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) {
fileStatus.status = 'completed';
fileStatus.error = null;
}
return;
}
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('filename', file.name);
formData.append('chunkNumber', currentChunk);
formData.append('totalChunks', totalChunks);
const xhr = new XMLHttpRequest();
xhr.open('POST', options.action, true);
xhr.upload.onprogress = updateProgress;
xhr.onload = () => {
uploadedBytes += chunk.size;
currentChunk++;
uploadChunk();
};
xhr.onerror = () => handleUploadError(file);
xhr.send(formData);
};
uploadChunk();
};
// 处理上传错误,重试或记录错误
const handleUploadError = (file) => {
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus.retryCount < maxRetries) {
fileStatus.retryCount++;
ElMessage.warning(`文件 ${file.name} 上传失败,重试第 ${fileStatus.retryCount} 次`);
customHttpRequest({
action: 'https://your-upload-endpoint',
file,
onProgress: (event) => console.log('progress:', event.percent),
onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
onError: () => handleUploadError(file),
});
} else {
fileStatus.status = 'failed';
fileStatus.error = '网络错误或服务器问题,上传失败';
ElMessage.error(`文件 ${file.name} 上传失败,请检查网络或稍后重试`);
}
};
// 重试上传
const retryUpload = (file) => {
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
fileStatus.retryCount = 0;
fileStatus.status = 'uploading';
fileStatus.error = null;
customHttpRequest({
action: 'https://your-upload-endpoint',
file,
onProgress: (event) => console.log('progress:', event.percent),
onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
onError: () => handleUploadError(file),
});
};
// 处理队列,限制同时上传数量
const processQueue = () => {
while (uploadQueue.length > 0 && currentUploads < maxConcurrentUploads) {
const file = uploadQueue.shift();
const fileStatus = uploadStatus.value.find((item) => item.uid === file.uid);
if (fileStatus) fileStatus.status = 'uploading';
customHttpRequest({
action: 'https://your-upload-endpoint',
file,
onProgress: (event) => console.log('progress:', event.percent),
onSuccess: () => ElMessage.success(`文件 ${file.name} 上传成功`),
onError: () => handleUploadError(file),
});
}
};
// 开始上传
const startUpload = () => {
processQueue();
};
// 初始化 Web Worker
onMounted(() => {
worker = new Worker(new URL('./imageWorker.js', import.meta.url));
});
onUnmounted(() => {
if (worker) worker.terminate();
});
</script>
<style>
.upload-item {
display: flex;
align-items: center;
margin-top: 10px;
}
.error-message {
color: red;
font-weight: bold;
}
</style>
- 错误处理和重试逻辑:
-
handleUploadError
方法对上传失败的文件进行错误处理。若retryCount
小于maxRetries
,则自动重试。- 超过最大重试次数时,将文件状态更新为
failed
并记录错误信息。
- 重试按钮:
-
- 在文件状态为
failed
时,显示“重试”按钮,用户可手动点击重新上传。 retryUpload
方法重置重试次数,并重新调用customHttpRequest
来重新上传失败文件。
- 在文件状态为
- 上传进度和状态管理:
-
- 每个文件的状态在
uploadStatus
中管理,状态包括pending
、uploading
、completed
和failed
,UI根据状态更新显示。
- 每个文件的状态在
- 精细的错误信息展示:
-
- 每个文件错误类型独立记录并展示在UI中,避免因多个文件错误导致混淆。
这样可以更好地支持批量上传过程中各个文件的精细管理、进度监控以及错误处理