脚本专家 发表于 2026-6-11 20:00:05

Vue3 + Element Plus 批量文件上传并发控制:队列调度、失败重试与分片上传

在 Vue 项目中实现批量文件上传时,并发问题是最常见的坑:浏览器默认同域并发连接数限制(通常 6 个)、服务器 QPS 限流、上传进度混乱、失败后难以重试等。本文针对不同场景给出四种实用的并发控制方案,所有代码基于 Vue3 组合式 API + Element Plus + axios,可直接复制到项目中调整使用。

核心痛点回顾
- 浏览器同域并发请求数有限(Chrome 约 6 个),超出后排队阻塞;
- 服务器瞬时大量请求可能被限流或拒绝;
- 无控制的并发导致进度统计不准,失败后重试成本高。

方案一:固定并发数 + 队列调度(基础版)
适用场景:中小批量文件(10-50 个)、单文件小于 100MB,追求简单易实现。

核心思路:将所有文件放入队列,设定最大并发数(建议 3-5),通过“正在上传计数”循环调度,一次只并发指定数量的文件。


<!-- 模板部分(Element Plus) -->
<el-upload
ref="uploadRef"
multiple
accept="*"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
>
<el-button type="primary">选择批量文件</el-button>
</el-upload>
<el-button type="success" @click="startBatchUpload" style="margin-top: 10px;">
开始上传
</el-button>
<div v-for="file in uploadStatus" :key="file.uid" style="margin-top: 10px;">
<span>{{ file.name }}</span>
<el-progress :percentage="file.progress" :status="file.status" style="width: 300px; margin-left: 10px;" />
</div>

<script setup>
import { ref } from 'vue';
import { ElUpload, ElButton, ElProgress, ElMessage } from 'element-plus';
import axios from 'axios';

const fileList = ref([]);
const uploadStatus = ref([]);
let uploadQueue = [];
let currentUploads = 0;
const maxConcurrent = 3; // 根据服务器调整
const uploadApi = '/api/upload/file';

const handleFileChange = (uploadFile) => {
// 去重
if (uploadQueue.some(item => item.uid === uploadFile.uid)) return;
uploadQueue.push(uploadFile);
uploadStatus.value.push({
    uid: uploadFile.uid,
    name: uploadFile.name,
    progress: 0,
    status: 'ready'
});
};

const uploadSingleFile = async (file) => {
const statusItem = uploadStatus.value.find(item => item.uid === file.uid);
statusItem.status = 'uploading';
const formData = new FormData();
formData.append('file', file.raw);
try {
    const response = await axios.post(uploadApi, formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress: (progressEvent) => {
      const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
      statusItem.progress = progress;
      }
    });
    statusItem.status = 'success';
    statusItem.progress = 100;
    ElMessage.success(`${file.name} 上传成功`);
    return response.data;
} catch (error) {
    statusItem.status = 'error';
    ElMessage.error(`${file.name} 上传失败,请重试`);
    throw error;
}
};

const processQueue = async () => {
while (currentUploads < maxConcurrent && uploadQueue.length > 0) {
    const file = uploadQueue.shift();
    currentUploads++;
    try {
      await uploadSingleFile(file);
    } catch (error) {
      // 可根据需要决定是否将失败文件放回队列头部
      // uploadQueue.unshift(file);
    } finally {
      currentUploads--;
      processQueue();
    }
}
};

const startBatchUpload = () => {
if (uploadQueue.length === 0) {
    ElMessage.warning('请先选择文件');
    return;
}
processQueue();
};
</script>


要点:上传完成(无论成功或失败)必须释放并发槽位(currentUploads--),并递归调用调度函数;maxConcurrent 建议根据服务器性能设置为 3-5,避免压垮服务器。

方案二:并发控制 + 失败重试(进阶版)
适用场景:文件较多(50-200 个)、网络不稳定、对成功率要求高。

在方案一的基础上增加重试机制与指数退避策略。


// 重试配置
const retryConfig = {
maxRetry: 3,
retryDelay: 1000 // 基础间隔 ms
};

// 重写 uploadSingleFile 函数
const uploadSingleFile = async (file, retryCount = 0) => {
const statusItem = uploadStatus.value.find(item => item.uid === file.uid);
statusItem.status = 'uploading';
const formData = new FormData();
formData.append('file', file.raw);
try {
    const response = await axios.post(uploadApi, formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      onUploadProgress: (progressEvent) => {
      const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
      statusItem.progress = progress;
      }
    });
    statusItem.status = 'success';
    statusItem.progress = 100;
    ElMessage.success(`${file.name} 上传成功`);
    return response.data;
} catch (error) {
    if (retryCount < retryConfig.maxRetry) {
      const delay = retryConfig.retryDelay * Math.pow(2, retryCount); // 指数退避
      statusItem.status = `retry(${retryCount + 1}/${retryConfig.maxRetry})`;
      ElMessage.warning(`${file.name} 上传失败,${delay}ms后重试(${retryCount + 1}/${retryConfig.maxRetry})`);
      await new Promise(resolve => setTimeout(resolve, delay));
      return uploadSingleFile(file, retryCount + 1);
    }
    statusItem.status = 'error';
    ElMessage.error(`${file.name} 上传失败,已达到最大重试次数`);
    throw error;
}
};

// 可选:取消所有上传
const cancelAllUpload = () => {
uploadQueue = [];
currentUploads = 0;
uploadStatus.value.forEach(item => {
    if (item.status === 'uploading' || item.status.includes('retry')) {
      item.status = 'cancelled';
    }
});
ElMessage.info('已取消所有上传任务');
};


核心改进:采用指数退避(1s→2s→4s)避免频繁重试增加服务器压力;retryCount 记录重试次数,超过上限后标记失败;新增全局取消功能提升用户体验。

方案三:分片上传 + 全局并发控制(高级版)
适用场景:批量大文件(单文件超过 100MB),需稳定高效上传。

将大文件切割为 1-5MB 的分片,通过全局并发限流器控制所有分片的总并发数。


// 分片配置
const chunkConfig = {
chunkSize: 1 * 1024 * 1024, // 1MB
maxConcurrent: 3
};

// 全局并发限流器
class ConcurrencyLimiter {
constructor(maxConcurrent) {
    this.maxConcurrent = maxConcurrent;
    this.activeCount = 0;
    this.pendingQueue = [];
}
enqueue(runTask, fileId) {
    return new Promise((resolve, reject) => {
      this.pendingQueue.push({ runTask, resolve, reject, fileId });
      this.tryStartNext();
    });
}
tryStartNext() {
    while (this.activeCount < this.maxConcurrent && this.pendingQueue.length > 0) {
      const { runTask, resolve, reject, fileId } = this.pendingQueue.shift();
      this.activeCount++;
      Promise.resolve()
      .then(() => runTask())
      .then(res => {
          this.activeCount--;
          resolve(res);
          this.tryStartNext();
      })
      .catch(err => {
          this.activeCount--;
          reject(err);
          this.tryStartNext();
      });
    }
}
}

const limiter = new ConcurrencyLimiter(chunkConfig.maxConcurrent);

// 生成文件唯一标识(用于分片合并、断点续传)
const getFileIdentifier = (file) => {
const originalFile = file._originalFile || file;
return `${originalFile.name}_${originalFile.size}_${originalFile.lastModified}`;
};

// 切割分片
const splitFileIntoChunks = (file) => {
const chunks = [];
let start = 0;
while (start < file.size) {
    const end = Math.min(start + chunkConfig.chunkSize, file.size);
    chunks.push(file.slice(start, end));
    start = end;
}
return chunks;
};

// 上传单个分片
const uploadChunk = async (chunk, fileId, chunkIndex, totalChunks) => {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
const response = await axios.post('/api/upload/chunk', formData, {
    headers: { 'Content-Type': 'multipart/form-data' }
});
return response.data;
};

// 上传大文件(分片并发)
const uploadLargeFile = async (file) => {
const fileId = getFileIdentifier(file);
const chunks = splitFileIntoChunks(file.raw);
const totalChunks = chunks.length;
const statusItem = uploadStatus.value.find(item => item.uid === file.uid);
statusItem.status = 'uploading';
const chunkTasks = chunks.map((chunk, index) => {
    return limiter.enqueue(() => uploadChunk(chunk, fileId, index, totalChunks), fileId)
      .then(() => {
      const progress = Math.round(((index + 1) / totalChunks) * 100);
      statusItem.progress = progress;
      });
});
try {
    await Promise.all(chunkTasks);
    await axios.post('/api/upload/merge', { fileId, fileName: file.name });
    statusItem.status = 'success';
    statusItem.progress = 100;
    ElMessage.success(`${file.name} 上传成功`);
} catch (error) {
    statusItem.status = 'error';
    ElMessage.error(`${file.name} 上传失败,请重试`);
    throw error;
}
};

// 修改队列调度,按文件大小选择分片或直接上传
const processQueue = async () => {
while (uploadQueue.length > 0) {
    const file = uploadQueue.shift();
    if (file.size > chunkConfig.chunkSize) {
      await uploadLargeFile(file);
    } else {
      await uploadSingleFile(file);
    }
}
};


要点:分片大小建议 1-5MB,太小增加请求数,太易超时;全局限流器确保所有文件的分片总并发不超标;需配合后端提供分片上传、合并接口。

方案四:极简版——使用成熟插件
若不想手写复杂逻辑,可直接使用 vue-upload-component(兼容 Vue2/3)或 Element Plus Upload 组件(自带并发控制)。

以 vue-upload-component 为例:

// 安装
npm install vue-upload-component --save

// 全局注册(main.js)
import Vue from 'vue';
import UploadComponent from 'vue-upload-component';
Vue.component('file-upload', UploadComponent);

// 页面使用
<template>
<file-upload
    v-model="files"
    url="/api/upload/file"
    :multiple="true"
    :concurrency="3"
    :retry="3"
    @input-file="handleFile"
>
    <el-button type="primary">批量上传</el-button>
</file-upload>
</template>
<script>
export default {
data() {
    return { files: [] };
},
methods: {
    handleFile(file) {
      console.log(file.progress, file.status);
    }
}
};
</script>

Element Plus 的 Upload 组件通过 limit 和 http-request 也可快速实现并发控制。

方案对比与选型建议
- 方案一:开发成本低,适合中小文件;
- 方案二:稳定性高,适合批量多、网络不固定场景;
- 方案三:支持大文件,但需前后端配合;
- 方案四:极速落地,无需定制化。

优化补充(必看)
- 前端:上传前校验文件大小和格式,减少无效请求;可用 Web Worker 切分文件,避免阻塞主线程;
- 后端:配置合适的文件大小限制和 QPS 限流,提供分片合并接口,支持文件去重;
- 进度优化:可增加全局平均进度条;单个进度基于上传字节或分片数计算;
- 跨域:后端 CORS 放行 multipart/form-data 请求。

总结:根据文件大小、数量和服务性能选择合适方案,亦可组合使用(如并发控制 + 分片上传 + 失败重试),实现高效稳定的批量文件上传。

热心网友6 发表于 2026-6-11 20:10:00

Re: Vue3 + Element Plus 批量文件上传并发控制:队列调度、失败重试与分片上传

感谢楼主的详细分享!方案一逻辑清晰,固定并发数加队列调度确实是中小批量上传最稳的入门写法。`currentUploads`计数器配合`processQueue`循环调度的思路也很直观,避免了手动管理Promise池的麻烦。 有几个小点想请教一下: 1. 失败重试部分,代码里注释了可以将失败文件放回队列头部,但实际项目里通常需要限制重试次数,否则网络抖动会导致死循环。楼主有没有建议的重试策略(比如指数退避)? 2. 对于单文件超过100MB的情况,方案一可能不太适用,标题里提到了分片上传,后续方案会涉及断点续传和分片并发吗?很期待看到相关实现。 3. 另外,`processQueue`函数里只展示了while循环的开始部分,后续的递归或异步控制(比如等待当前任务完成后再调度)是怎样的?是直接`processQueue()`递归调用,还是监听`currentUploads`变化? 感谢分享,已经收藏了,坐等方案二到四!

热心网友2 发表于 2026-6-20 10:05:00

Re: Vue3 + Element Plus 批量文件上传并发控制:队列调度、失败重试与分片上传

感谢楼主分享,这个并发控制的方案非常实用,代码也很清晰,可以直接拿来用。固定并发数配合队列调度确实能有效解决浏览器连接数限制和服务器压力问题,而且进度统计也更准确了。 有个小疑问想请教:代码里失败后只是弹窗提示,没有自动重试逻辑。实际项目中如果网络抖动导致部分文件失败,是否考虑在方案里加入重试次数限制(比如重试3次)?另外,您提到后续还有另外三种方案,方便提前透露一下分片上传和失败重试的设计思路吗?期待后续更新。

热心网友5 发表于 2026-6-20 15:05:00

Re: Vue3 + Element Plus 批量文件上传并发控制:队列调度、失败重试与分片上传

感谢分享!很实用的批量上传并发控制方案。方案一的队列调度思路清晰,代码也直接可用。不过注意到代码最后好像没写完?另外想请教一下,如果文件上传失败,您提到可以放回队列头部,但这样可能会导致同一个文件反复重试直到成功,有没有设置最大重试次数的考虑?期待后续的几种方案。
页: [1]
查看完整版本: Vue3 + Element Plus 批量文件上传并发控制:队列调度、失败重试与分片上传