Vue 大文件分片上传组件实现解析
一、功能概述
1.1本组件基于 Vue + Element UI 实现,主要功能特点:
- 大文件分片上传:支持 2MB 分片切割上传
- 实时进度显示:可视化展示每个文件上传进度
- 智能格式校验:支持文件类型、大小、特殊字符校验
- 文件预览删除:已上传文件可预览和删除
- 断点续传能力:网络中断后可恢复上传
- 失败自动重试:分片级失败重试机制(最大3次)
用户选择文件 → 前端校验 → 分片切割 → 并行上传 → 合并确认 → 完成上传
二、核心实现解析
2.1 分片上传机制
// 分片切割逻辑
const chunkSize = 2 * 1024 * 1024 // 2MB分片
const totalChunks = Math.ceil(file.size / chunkSize)for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {const start = (chunkNumber - 1) * chunkSizeconst 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 断点续传实现
// 使用Map存储上传记录
uploadedChunksMap = new Map() // 上传前检查已传分片
if (!uploadedChunks.has(chunkNumber)) {// 执行上传
}// 上传成功记录分片
uploadedChunks.add(chunkNumber)
2.3 智能重试机制
const maxRetries = 3 // 最大重试次数
const baseDelay = 1000 // 基础延迟// 指数退避算法
const delay = Math.min(baseDelay * Math.pow(2, retries - 1) + Math.random() * 1000, 10000
)
三、关键代码详解
3.1 文件标识生成
createFileIdentifier(file) {// 文件名 + 大小 + 时间戳 生成唯一IDreturn `${file.name}-${file.size}-${new Date().getTime()}`
}
3.2 进度计算原理
// 实时更新进度
this.$set(this.uploadProgress, file.name, Math.floor((uploadedChunks.size / totalChunks) * 100))
3.3 文件校验体系
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 并发上传控制
// 设置并行上传数
const parallelUploads = 3
const uploadQueue = []for (let i=0; i<parallelUploads; i++) {uploadQueue.push(uploadNextChunk())
}await Promise.all(uploadQueue)
5.2 内存优化策略
复制
// 分片上传后立即释放内存
chunk = null
formData = null
5.3 秒传功能实现
// 计算文件哈希值
const fileHash = await calculateMD5(file)// 查询服务器是否存在相同文件
const res = await checkFileExist(fileHash)
if (res.exist) {this.handleUploadSuccess(res)return
}
六、错误处理机制
6.1 常见错误类型
七、完整版代码
7.1 代码
<template><div class="upload-file"><el-uploadmultiple: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-groupclass="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><!-- 上传进度展示 --><divv-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 filecreateFileIdentifier(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使用说明
<FileUpload v-model="fileUrls":limit="3":fileSize="10":fileType="['pdf','docx']"
/>
该组件为Vue应用提供了一个可靠的大文件上传解决方案,结合分块、断点续传和进度显示,显著提升了用户体验和上传成功率。适合集成到需要处理大文件或弱网环境的系统中