Vue大文件分片上传组件实现解析及关键代码
作者:嘴巴嘟嘟
在开发中,如果上传的文件过大,可以考虑分片上传,分片就是说将文件拆分来进行上传,将各个文件的切片传递给后台,然后后台再进行合并,这篇文章主要介绍了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) * 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 断点续传实现
// 使用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) { // 文件名 + 大小 + 时间戳 生成唯一ID return `${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-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}`" rel="external nofollow" :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使用说明
<FileUpload v-model="fileUrls" :limit="3" :fileSize="10" :fileType="['pdf','docx']" />
该组件为Vue应用提供了一个可靠的大文件上传解决方案,结合分块、断点续传和进度显示,显著提升了用户体验和上传成功率。适合集成到需要处理大文件或弱网环境的系统中
总结
到此这篇关于Vue大文件分片上传组件实现解析及关键代码的文章就介绍到这了,更多相关Vue大文件分片上传组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!