Vue 大文件分片上传组件实现解析
一、功能概述
1.1本组件基于 Vue + Element UI 实现,主要功能特点:
- 大文件分片上传:支持 2MB 分片切割上传
- 实时进度显示:可视化展示每个文件上传进度
- 智能格式校验:支持文件类型、大小、特殊字符校验
- 文件预览删除:已上传文件可预览和删除
- 断点续传能力:网络中断后可恢复上传
- 失败自动重试:分片级失败重试机制(最大3次)
用户选择文件 → 前端校验 → 分片切割 → 并行上传 → 合并确认 → 完成上传
二、核心实现解析
2.1 分片上传机制
javascript
// 分片切割逻辑
const chunkSize = 2 * 1024 * 1024 // 2MB分片
const totalChunks = Math.ceil(file.size / chunkSize)
for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
const start = (chunkNumber - 1) * 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('chunkNumber', chunkNumber)
formData.append('totalChunks', totalChunks)
}
2.2 断点续传实现
javascript
// 使用Map存储上传记录
uploadedChunksMap = new Map()
// 上传前检查已传分片
if (!uploadedChunks.has(chunkNumber)) {
// 执行上传
}
// 上传成功记录分片
uploadedChunks.add(chunkNumber)
2.3 智能重试机制
javascript
const maxRetries = 3 // 最大重试次数
const baseDelay = 1000 // 基础延迟
// 指数退避算法
const delay = Math.min(
baseDelay * Math.pow(2, retries - 1) + Math.random() * 1000,
10000
)
三、关键代码详解
3.1 文件标识生成
javascript
createFileIdentifier(file) {
// 文件名 + 大小 + 时间戳 生成唯一ID
return `${file.name}-${file.size}-${new Date().getTime()}`
}
3.2 进度计算原理
javascript
// 实时更新进度
this.$set(this.uploadProgress, file.name,
Math.floor((uploadedChunks.size / totalChunks) * 100))
3.3 文件校验体系
javascript
handleBeforeUpload(file) {
// 类型校验
const fileExt = file.name.split('.').pop()
if (!this.fileType.includes(fileExt)) return false
// 特殊字符校验
if (file.name.includes(',')) return false
// 大小校验(MB转换)
return file.size / 1024 / 1024 < this.fileSize
}
四、服务端对接指南
4.1 必要接口清单

五、性能优化建议
5.1 并发上传控制
javascript
// 设置并行上传数
const parallelUploads = 3
const uploadQueue = []
for (let i=0; i<parallelUploads; i++) {
uploadQueue.push(uploadNextChunk())
}
await Promise.all(uploadQueue)
5.2 内存优化策略
javascript
复制
// 分片上传后立即释放内存
chunk = null
formData = null
5.3 秒传功能实现
javascript
// 计算文件哈希值
const fileHash = await calculateMD5(file)
// 查询服务器是否存在相同文件
const res = await checkFileExist(fileHash)
if (res.exist) {
this.handleUploadSuccess(res)
return
}
六、错误处理机制
6.1 常见错误类型

七、完整版代码
7.1 代码
javascript
<template>
<div class="upload-file">
<el-upload
multiple
:action="'#'"
:http-request="customUpload"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType.length > 0">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件
</div>
</el-upload>
<!-- 文件列表 -->
<transition-group
class="upload-file-list el-upload-list el-upload-list--text"
name="el-fade-in-linear"
tag="ul"
>
<li
:key="file.url"
class="el-upload-list__item ele-upload-list__item-content"
v-for="(file, index) in fileList"
>
<el-link
:href="`${baseUrl}${file.url}`"
:underline="false"
target="_blank"
>
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link
:underline="false"
@click="handleDelete(index)"
type="danger"
v-if="!disabled"
>删除</el-link
>
</div>
</li>
</transition-group>
<!-- 上传进度展示 -->
<div
v-for="(progress, fileName) in uploadProgress"
:key="fileName"
class="upload-progress"
>
<div class="progress-info">
<span class="file-name">{{ fileName }}</span>
<span class="percentage">{{ progress }}%</span>
</div>
<el-progress :percentage="progress" :show-text="false"></el-progress>
</div>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
import { uploadFileProgress } from "@/api/resource";
export default {
name: "FileUpload",
props: {
// 值
value: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => [
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"txt",
"pdf",
],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
// 禁用组件(仅查看文件)
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
number: 0,
uploadList: [],
baseUrl: process.env.VUE_APP_BASE_API,
uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传文件服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: [],
uploadProgress: {}, // 存储文件上传进度
uploadedChunksMap: new Map(), // 新增:存储每个文件的已上传分片记录
};
},
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
// 首先将值转为数组
const list = Array.isArray(val) ? val : this.value.split(",");
// 然后将数组转为对象数组
this.fileList = list.map((item) => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true,
},
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件类型
if (this.fileType && this.fileType.length > 0) {
const fileName = file.name.split(".");
const fileExt = fileName[fileName.length - 1];
const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
this.$modal.msgError(
`文件格式不正确,请上传${this.fileType.join("/")}格式文件!`
);
return false;
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(",")) {
this.$modal.msgError("文件名不正确,不能包含英文逗号!");
return false;
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
// this.$modal.loading("正在上传文件,请稍候...");
this.number++;
return true;
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError(err) {
// 确保在上传错误时移除进度条
if (err.file && err.file.name) {
this.$delete(this.uploadProgress, err.file.name);
}
this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading();
},
// 上传成功回调
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.fileUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
// 删除文件
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", this.listToString(this.fileList));
},
// 上传结束处理
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
// 获取文件名称
getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
},
// Create unique identifier for file
createFileIdentifier(file) {
return `${file.name}-${file.size}-${new Date().getTime()}`;
},
async customUpload({ file }) {
try {
const chunkSize = 2 * 1024 * 1024;
const totalChunks = Math.ceil(file.size / chunkSize);
const identifier = this.createFileIdentifier(file);
const maxRetries = 3;
const baseDelay = 1000;
// 获取或创建该文件的已上传分片记录
if (!this.uploadedChunksMap.has(identifier)) {
this.uploadedChunksMap.set(identifier, new Set());
}
const uploadedChunks = this.uploadedChunksMap.get(identifier);
this.$set(this.uploadProgress, file.name,
Math.floor((uploadedChunks.size / totalChunks) * 100));
for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
// 如果分片已上传成功,跳过
if (uploadedChunks.has(chunkNumber)) {
continue;
}
let currentChunkSuccess = false;
let retries = 0;
while (!currentChunkSuccess && retries < maxRetries) {
try {
const start = (chunkNumber - 1) * 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('identifier', identifier);
formData.append('totalChunks', totalChunks);
formData.append('chunkNumber', chunkNumber);
formData.append('fileName', file.name);
const res = await uploadFileProgress(formData);
if (res.code !== 200) {
throw new Error(res.msg || '上传失败');
}
uploadedChunks.add(chunkNumber);
this.$set(this.uploadProgress, file.name,
Math.floor((uploadedChunks.size / totalChunks) * 100));
currentChunkSuccess = true;
// 所有分片上传完成
if (uploadedChunks.size === totalChunks) {
const successRes = {
code: 200,
fileName: res.fileName,
url: res.url,
};
// 清理该文件的上传记录
this.uploadedChunksMap.delete(identifier);
// 立即移除进度条
this.$delete(this.uploadProgress, file.name);
this.handleUploadSuccess(successRes, file);
return;
}
} catch (error) {
retries++;
// if (retries === maxRetries) {
// throw new Error(`分片 ${chunkNumber} 上传失败,已重试 ${maxRetries} 次`);
// }
const delay = Math.min(baseDelay * Math.pow(2, retries - 1) + Math.random() * 1000, 10000);
// this.$message.warning(`分片 ${chunkNumber} 上传失败,${retries}秒后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
if (!currentChunkSuccess) {
throw new Error(`分片 ${chunkNumber} 上传失败`);
}
}
} catch (error) {
// 确保在错误时也移除进度条
this.$delete(this.uploadProgress, file.name);
this.uploadedChunksMap.delete(identifier); // 清理上传记录
this.$modal.closeLoading();
this.$modal.msgError(error.message || '上传文件失败,请重试');
}
},
},
};
</script>
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
.upload-progress {
margin: 10px 0;
padding: 8px 12px;
background-color: #f5f7fa;
border-radius: 4px;
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.file-name {
color: #606266;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 80%;
}
.percentage {
color: #409eff;
font-size: 13px;
font-weight: 500;
}
}
.el-progress {
margin-bottom: 4px;
}
}
</style>
7.2使用说明
javascript
<FileUpload
v-model="fileUrls"
:limit="3"
:fileSize="10"
:fileType="['pdf','docx']"
/>
该组件为Vue应用提供了一个可靠的大文件上传解决方案,结合分块、断点续传和进度显示,显著提升了用户体验和上传成功率。适合集成到需要处理大文件或弱网环境的系统中