图片批量上传难点和亮点

使用场景

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实现图片压缩,并将图片格式转换为JPEGPNG等常见格式。

// 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监听预处理完成后的文件,并将其加入到上传队列。
  • 自定义上传和并发控制:通过customHttpRequestprocessQueue方法,控制同时上传的数量,确保不会超出maxConcurrentUploads的限制。

第三点优化

实现上传进度管理的UI展示断点续传多文件进度管理,我们可以做以下几项优化:

  1. 上传进度展示:为每张图片添加进度条,实时显示上传进度。
  2. 断点续传:对已上传的数据做断点标记,当上传中断时可以从中断的部分继续上传。
  3. 多文件进度管理:管理每个文件的上传进度状态,并在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 }}&nbsp;
        <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>
  1. 错误处理和重试逻辑
    • handleUploadError 方法对上传失败的文件进行错误处理。若retryCount小于maxRetries,则自动重试。
    • 超过最大重试次数时,将文件状态更新为failed并记录错误信息。
  1. 重试按钮
    • 在文件状态为failed时,显示“重试”按钮,用户可手动点击重新上传。
    • retryUpload 方法重置重试次数,并重新调用customHttpRequest来重新上传失败文件。
  1. 上传进度和状态管理
    • 每个文件的状态在uploadStatus中管理,状态包括pendinguploadingcompletedfailed,UI根据状态更新显示。
  1. 精细的错误信息展示
    • 每个文件错误类型独立记录并展示在UI中,避免因多个文件错误导致混淆。

这样可以更好地支持批量上传过程中各个文件的精细管理、进度监控以及错误处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值