Vue大文件分片上傳組件實(shí)現(xiàn)解析及關(guān)鍵代碼
一、功能概述
1.1本組件基于 Vue + Element UI 實(shí)現(xiàn),主要功能特點(diǎn):
- 大文件分片上傳:支持 2MB 分片切割上傳
- 實(shí)時(shí)進(jìn)度顯示:可視化展示每個(gè)文件上傳進(jìn)度
- 智能格式校驗(yàn):支持文件類型、大小、特殊字符校驗(yàn)
- 文件預(yù)覽刪除:已上傳文件可預(yù)覽和刪除
- 斷點(diǎn)續(xù)傳能力:網(wǎng)絡(luò)中斷后可恢復(fù)上傳
- 失敗自動(dòng)重試:分片級(jí)失敗重試機(jī)制(最大3次)
用戶選擇文件 → 前端校驗(yàn) → 分片切割 → 并行上傳 → 合并確認(rèn) → 完成上傳
二、核心實(shí)現(xiàn)解析
2.1 分片上傳機(jī)制
// 分片切割邏輯
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)
// 構(gòu)造分片數(shù)據(jù)包
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkNumber', chunkNumber)
formData.append('totalChunks', totalChunks)
}
2.2 斷點(diǎn)續(xù)傳實(shí)現(xiàn)
// 使用Map存儲(chǔ)上傳記錄
uploadedChunksMap = new Map()
// 上傳前檢查已傳分片
if (!uploadedChunks.has(chunkNumber)) {
// 執(zhí)行上傳
}
// 上傳成功記錄分片
uploadedChunks.add(chunkNumber)
2.3 智能重試機(jī)制
const maxRetries = 3 // 最大重試次數(shù) const baseDelay = 1000 // 基礎(chǔ)延遲 // 指數(shù)退避算法 const delay = Math.min( baseDelay * Math.pow(2, retries - 1) + Math.random() * 1000, 10000 )
三、關(guān)鍵代碼詳解
3.1 文件標(biāo)識(shí)生成
createFileIdentifier(file) {
// 文件名 + 大小 + 時(shí)間戳 生成唯一ID
return `${file.name}-${file.size}-${new Date().getTime()}`
}
3.2 進(jìn)度計(jì)算原理
// 實(shí)時(shí)更新進(jìn)度 this.$set(this.uploadProgress, file.name, Math.floor((uploadedChunks.size / totalChunks) * 100))
3.3 文件校驗(yàn)體系
handleBeforeUpload(file) {
// 類型校驗(yàn)
const fileExt = file.name.split('.').pop()
if (!this.fileType.includes(fileExt)) return false
// 特殊字符校驗(yàn)
if (file.name.includes(',')) return false
// 大小校驗(yàn)(MB轉(zhuǎn)換)
return file.size / 1024 / 1024 < this.fileSize
}
四、服務(wù)端對(duì)接指南
4.1 必要接口清單

五、性能優(yōu)化建議
5.1 并發(fā)上傳控制
// 設(shè)置并行上傳數(shù)
const parallelUploads = 3
const uploadQueue = []
for (let i=0; i<parallelUploads; i++) {
uploadQueue.push(uploadNextChunk())
}
await Promise.all(uploadQueue)
5.2 內(nèi)存優(yōu)化策略
// 分片上傳后立即釋放內(nèi)存 chunk = null formData = null
5.3 秒傳功能實(shí)現(xiàn)
// 計(jì)算文件哈希值
const fileHash = await calculateMD5(file)
// 查詢服務(wù)器是否存在相同文件
const res = await checkFileExist(fileHash)
if (res.exist) {
this.handleUploadSuccess(res)
return
}
六、錯(cuò)誤處理機(jī)制
6.1 常見錯(cuò)誤類型

七、完整版代碼
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">
請(qǐng)上傳
<template v-if="fileSize">
大小不超過(guò) <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>
<!-- 上傳進(jìn)度展示 -->
<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],
// 數(shù)量限制
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", // 上傳文件服務(wù)器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: [],
uploadProgress: {}, // 存儲(chǔ)文件上傳進(jìn)度
uploadedChunksMap: new Map(), // 新增:存儲(chǔ)每個(gè)文件的已上傳分片記錄
};
},
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
// 首先將值轉(zhuǎn)為數(shù)組
const list = Array.isArray(val) ? val : this.value.split(",");
// 然后將數(shù)組轉(zhuǎn)為對(duì)象數(shù)組
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(
`文件格式不正確,請(qǐng)上傳${this.fileType.join("/")}格式文件!`
);
return false;
}
}
// 校檢文件名是否包含特殊字符
if (file.name.includes(",")) {
this.$modal.msgError("文件名不正確,不能包含英文逗號(hào)!");
return false;
}
// 校檢文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上傳文件大小不能超過(guò) ${this.fileSize} MB!`);
return false;
}
}
// this.$modal.loading("正在上傳文件,請(qǐng)稍候...");
this.number++;
return true;
},
// 文件個(gè)數(shù)超出
handleExceed() {
this.$modal.msgError(`上傳文件數(shù)量不能超過(guò) ${this.limit} 個(gè)!`);
},
// 上傳失敗
handleUploadError(err) {
// 確保在上傳錯(cuò)誤時(shí)移除進(jìn)度條
if (err.file && err.file.name) {
this.$delete(this.uploadProgress, err.file.name);
}
this.$modal.msgError("上傳文件失敗,請(qǐng)重試");
this.$modal.closeLoading();
},
// 上傳成功回調(diào)
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));
},
// 上傳結(jié)束處理
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;
}
},
// 對(duì)象轉(zhuǎn)成指定字符串分隔
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;
// 獲取或創(chuàng)建該文件的已上傳分片記錄
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++) {
// 如果分片已上傳成功,跳過(guò)
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);
// 立即移除進(jìn)度條
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) {
// 確保在錯(cuò)誤時(shí)也移除進(jìn)度條
this.$delete(this.uploadProgress, file.name);
this.uploadedChunksMap.delete(identifier); // 清理上傳記錄
this.$modal.closeLoading();
this.$modal.msgError(error.message || '上傳文件失敗,請(qǐng)重試');
}
},
},
};
</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使用說(shuō)明
<FileUpload v-model="fileUrls" :limit="3" :fileSize="10" :fileType="['pdf','docx']" />
該組件為Vue應(yīng)用提供了一個(gè)可靠的大文件上傳解決方案,結(jié)合分塊、斷點(diǎn)續(xù)傳和進(jìn)度顯示,顯著提升了用戶體驗(yàn)和上傳成功率。適合集成到需要處理大文件或弱網(wǎng)環(huán)境的系統(tǒng)中
總結(jié)
到此這篇關(guān)于Vue大文件分片上傳組件實(shí)現(xiàn)解析及關(guān)鍵代碼的文章就介紹到這了,更多相關(guān)Vue大文件分片上傳組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Element UI 自定義正則表達(dá)式驗(yàn)證方法
今天小編就為大家分享一篇Element UI 自定義正則表達(dá)式驗(yàn)證方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
vue2基本響應(yīng)式實(shí)現(xiàn)方式之讓數(shù)組也變成響應(yīng)式
這篇文章主要介紹了vue2基本響應(yīng)式實(shí)現(xiàn)方式之讓數(shù)組也變成響應(yīng)式問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
vue Element-ui input 遠(yuǎn)程搜索與修改建議顯示模版的示例代碼
本文分為html,js和css代碼給大家詳細(xì)介紹了vue Element-ui input 遠(yuǎn)程搜索與修改建議顯示模版功能,感興趣的朋友一起看看吧2017-10-10
vue-video-player 斷點(diǎn)續(xù)播的實(shí)現(xiàn)
這篇文章主要介紹了vue-video-player 斷點(diǎn)續(xù)播的實(shí)現(xiàn),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
在vue3項(xiàng)目中給頁(yè)面添加水印的實(shí)現(xiàn)方法
這篇文章主要給大家介紹一下在vue3項(xiàng)目中添加水印的實(shí)現(xiàn)方法,文中有詳細(xì)的代碼示例供大家參考,具有一定的參考價(jià)值,感興趣的小伙伴跟著小編一起來(lái)看看吧2023-08-08
在Vue開發(fā)過(guò)程中解決和預(yù)防內(nèi)存泄漏問(wèn)題的方法詳解
Vue作為一款流行的前端框架,已經(jīng)在許多項(xiàng)目中得到廣泛應(yīng)用,然而,隨著我們?cè)赩ue中構(gòu)建更大規(guī)模的應(yīng)用程序,我們可能會(huì)遇到一個(gè)嚴(yán)重的問(wèn)題,那就是內(nèi)存泄漏,因此,我們需要認(rèn)識(shí)到在Vue開發(fā)過(guò)程中,內(nèi)存泄漏問(wèn)題的重要性,本文將給大家介紹如何解決和預(yù)防內(nèi)存泄漏問(wèn)題2023-10-10
vue3之Suspense加載異步數(shù)據(jù)的使用
本文主要介紹了vue3之Suspense加載異步數(shù)據(jù)的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02

