前端大文件分片MinIO上傳的詳細代碼
大文件分片上傳是一種將大文件拆分成多個小文件片段進行上傳的技術(shù)。這種方式可以提高上傳效率,減少網(wǎng)絡(luò)傳輸時間,并且在網(wǎng)絡(luò)不穩(wěn)定或者上傳過程中出現(xiàn)錯誤時,可以更容易地恢復(fù)上傳進度。
大文件分片上傳的步驟如下:
- 將大文件分成多個固定大小的片段,通常每個片段的大小在幾十KB到幾MB之間。
- 逐個上傳每個文件片段,可以使用HTTP、FTP等協(xié)議進行傳輸。
- 服務(wù)器接收到每個文件片段后,判斷其MD5值進行保存或者合并操作。
- 在上傳過程中,通過MD5值維護一個上傳進度記錄,標記已經(jīng)上傳成功的文件片段,以便在上傳中斷后能夠恢復(fù)上傳進度。
- 當所有文件片段都上傳完成后,服務(wù)器將文件片段進行合并,得到完整的大文件。
大文件分片上傳的好處有:
- 提高上傳速度:將大文件拆分成小片段,可以同時上傳多個片段,從而提高上傳速度。
- 斷點續(xù)傳:如果在上傳過程中發(fā)生中斷或者錯誤,可以根據(jù)上傳進度記錄,只重新上傳丟失或者出錯的文件片段,從而減少網(wǎng)絡(luò)傳輸時間。
- 易于管理:將大文件拆分成小片段,可以更方便地管理和存儲,避免了一次性上傳整個大文件可能導(dǎo)致的內(nèi)存占用問題。
大文件分片上傳技術(shù)已經(jīng)廣泛應(yīng)用于各種云存儲、文件傳輸?shù)阮I(lǐng)域,為用戶提供了更好的上傳體驗和效率。
視圖代碼
大文件分片需要讀取時間所以要給加載狀態(tài),下面例子只適合單文件上傳且?guī)蟼鬟M度展示
<template> <div class="slice-upload" v-loading="loading" element-loading-text="文件分片讀取中" element-loading-spinner="el-icon-loading"> <form id="fromCont" method="post" style="display: inline-block"> <el-button size="small" @click="inputChange" class="file-choose-btn" :disabled="uploading"> 選擇文件 <input v-show="false" id="file" ref="fileValue" :accept="accept" type="file" @change="choseFile" /> </el-button> </form> <slot name="status"></slot> <div class="el-upload__tip"> 請上傳不超過 <span style="color: #e6a23c">{{ maxCalc }}</span> 的文件 </div> <div class="file-list"> <transition name="list" tag="p"> <div v-if="file" class="list-item"> <i class="el-icon-document mr5"></i> <span>{{ file.name }} <em v-show="uploading" style="color: #67c23a">上傳中....</em></span> <span class="percentage">{{ percentage }}%</span> <el-progress :show-text="false" :text-inside="false" :stroke-width="2" :percentage="percentage" /> </div> </transition> </div> </div> </template>
邏輯代碼
需要引入Md5
npm install spark-md5
<script> import SparkMD5 from "spark-md5"; import axios from "axios"; import { getImagecheckFile,//檢驗是否上傳過用于斷點續(xù)傳 Imageinit,//用分片換取minIo上傳地址 Imagecomplete,//合并分片 } from "/*接口地址*/"; export default { name: "sliceUpload", /** * 外部數(shù)據(jù) * @type {Object} */ props: { /** * @Description * 代碼注釋說明 * 接口url * @Return */ findFileUrl: String, continueUrl: String, finishUrl: String, removeUrl: String, /** * @Description * 代碼注釋說明 * 最大上傳文件大小 100G * @Return */ maxFileSize: { type: Number, default: 100 * 1024 * 1024 * 1024, }, /** * @Description * 代碼注釋說明 * 切片大小 * @Return */ sliceSize: { type: Number, default: 50 * 1024 * 1024, }, /** * @Description * 代碼注釋說明 * 是否可以上傳 * @Return */ show: { type: Boolean, default: true, }, accept: String, }, /** * 數(shù)據(jù)定義 * @type {Object} */ data() { return { /** * @Description * 代碼注釋說明 * 文件 * @Return */ file: null,//源文件 imageSize: 0,//文件大小單位GB uploadId: "",//上傳id fullPath: "",//上傳地址 uploadUrls: [],//分片上傳地址集合 hash: "",//文件MD5 /** * @Description * 代碼注釋說明 * 分片文件 * @Return */ formDataList: [], /** * @Description * 代碼注釋說明 * 未上傳分片 * @Return */ waitUpLoad: [], /** * @Description * 代碼注釋說明 * 未上傳個數(shù) * @Return */ waitNum: NaN, /** * @Description * 代碼注釋說明 * 上傳大小限制 * @Return */ limitFileSize: false, /** * @Description * 代碼注釋說明 * 進度條 * @Return */ percentage: 0, percentageFlage: false, /** * @Description * 代碼注釋說明 * 讀取loading * @Return */ loading: false, /** * @Description * 代碼注釋說明 * 正在上傳中 * @Return */ uploading: false, /** * @Description * 代碼注釋說明 * 暫停上傳 * @Return */ stoped: false, /** * @Description * 代碼注釋說明 * 上傳后的文件數(shù)據(jù) * @Return */ fileData: { id: "", path: "", }, }; }, /** * 數(shù)據(jù)監(jiān)聽 * @type {Object} */ watch: { //監(jiān)控上傳進度 waitNum: { handler(v, oldVal) { let p = Math.floor( ((this.formDataList.length - v) / this.formDataList.length) * 100 ); // debugger; this.percentage = p > 100 ? 100 : p; }, deep: true, }, show: { handler(v, oldVal) { if (!v) { this.file = null } }, deep: true, }, }, /** * 方法集合 * @type {Object} */ methods: { /** * 代碼注釋說明 * 內(nèi)存過濾器 * @param {[type]} ram [description] * @return {[type]} [description] */ ramFilter(bytes) { if (bytes === 0) return "0"; var k = 1024; let sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; let i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i]; }, /** * 觸發(fā)上傳 文件處理 * @param e */ async choseFile(e) { const fileInput = e.target.files[0]; // 獲取當前文件 this.imageSize = this.ramFilter(fileInput.size);//記錄文件大小 if (!fileInput && !this.show) { return; } const pattern = /[\u4e00-\u9fa5]/; if (pattern.test(fileInput?.name)) { this.$message.warning("請不要上傳帶有中文名稱的鏡像文件!"); return; } this.file = fileInput; // file 丟全局方便后面用 可以改進為func傳參形式 this.percentage = 0; if (this.file.size < this.maxFileSize) { this.loading = true; const FileSliceCap = this.sliceSize; // 分片字節(jié)數(shù) let start = 0; // 定義分片開始切的地方 let end = 0; // 每片結(jié)束切的地方a let i = 0; // 第幾片 this.formDataList = []; // 分片存儲的一個池子 丟全局 this.waitUpLoad = []; // 分片存儲的一個池子 丟全局 while (end < this.file.size && this.show) { /** * @Description * 代碼注釋說明 * 當結(jié)尾數(shù)字大于文件總size的時候 結(jié)束切片 * @Return */ start = i * FileSliceCap; // 計算每片開始位置 end = (i + 1) * FileSliceCap; // 計算每片結(jié)束位置 var fileSlice = this.file.slice(start, end); // 開始切 file.slice 為 h5方法 對文件切片 參數(shù)為 起止字節(jié)數(shù) const formData = new window.FormData(); // 創(chuàng)建FormData用于存儲傳給后端的信息 // formData.append('fileMd5', this.fileMd5) // 存儲總文件的Md5 讓后端知道自己是誰的切片 formData.append("file", fileSlice); // 當前的切片 formData.append("chunkNumber", i); // 當前是第幾片 formData.append("fileName", this.file.name); // 當前文件的文件名 用于后端文件切片的命名 formData.appen 為 formData對象添加參數(shù)的方法 this.formDataList.push({ key: i, formData }); // 把當前切片信息 自己是第幾片 存入我們方才準備好的池子 i++; } //獲取文件的MD5值 this.computeFileMD5(this.file, FileSliceCap).then( (res) => { if (res) { this.hash = res; //console.log("拿到了:", res); // this.UploadStatus = `文件讀取成功(${res}),文件上傳中...`; //通過Md5值查詢是否上傳過 getImagecheckFile({ fileCode: res }).then( (res2) => { this.loading = false; /** * @Description * 代碼注釋說明 * 全部切完以后 發(fā)一個請求給后端 拉當前文件后臺存儲的切片信息 用于檢測有多少上傳成功的切片 * fileUrl:有地址就是秒傳因為已經(jīng)存在該文件了 * shardingIndex:返回哪些已經(jīng)上傳用于斷點續(xù)傳 * @Return */ let { fileUrl, shardingIndex } = res2.data.data; //檢測是否上傳過 if (!fileUrl) { /** * @Description * 代碼注釋說明 * 當是斷點續(xù)傳時候 * 記得處理一下當前是默認全都沒上傳過暫不支持斷點續(xù)傳后端無法返回已上傳數(shù)據(jù)如果你家后端牛一點可以在此處理斷點續(xù)傳 * @Return */ this.waitUpLoad = this.formDataList;//當前是默認全都沒上傳過斷點續(xù)傳需處理 this.getFile() } else { // debugger; this.formDataList = [{ key: fileUrl }]; this.waitNum = 1; this.waitUpLoad = []; // 秒傳則沒有需要上傳的切片 this.$message.success("文件已秒傳"); this.$emit("fileinput", { url: fileUrl, code: this.hash, imageSize: this.imageSize, }); this.waitNum = 0; // this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.loading = false; return; } this.waitNum = this.waitUpLoad.length; // 記錄長度用于百分比展示 }, (err) => { this.$message.error("獲取文件數(shù)據(jù)失敗"); this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.loading = false; return; } ); } else { // this.UploadStatus = "文件讀取失敗"; } }, (err) => { // this.UploadStatus = "文件讀取失敗"; this.uploading = false; this.loading = false; this.$message.error("文件讀取失敗"); } ); } else { //this.limitFileSize = true; this.$message.error("請上傳小于100G的文件"); this.file = null; this.$refs.fileValue.value = '' this.uploading = false; } }, //準備上傳 getFile() { /** * @Description * 代碼注釋說明 * 確定按鈕 * @Return */ if (this.file === null) { this.$message.error("請先上傳文件"); return; } this.percentageFlage = this.percentage == 100; this.sliceFile(); // 上傳切片 }, async sliceFile() { /** * @Description * 代碼注釋說明 * 如果已上傳文件且生成了文件路徑 * @Return */ if (this.fileData.path) { return; } /** * @Description * 代碼注釋說明 * 如果是切片已全部上傳 但還未完成合并及移除chunk操作 沒有生成文件路徑時 * @Return */ if (this.percentageFlage && !this.fileData.path) { this.finishUpload(); return; } this.uploading = true; this.stoped = false; //提交切片 this.upLoadFileSlice(); }, async upLoadFileSlice() { if (this.stoped) { this.uploading = false; return; } /** * @Description * 代碼注釋說明 * 剩余切片數(shù)為0時調(diào)用結(jié)束上傳接口 * @Return */ try { let suffix = /\.([0-9A-z]+)$/.exec(this.file.name)[1]; // 文件后綴名也就是文件類型 let data = { bucketName: "static",//桶的名字 contentType: this.file.type || suffix,//文件類型 filename: this.file.name,//文件名字 partCount: this.waitUpLoad.length,//分片多少也就是分了多少個 }; //根據(jù)分片長度獲取分片上傳地址以及上傳ID和文件地址 Imageinit(data).then((res) => { if (res.data.code == 200 && res.data.data) { this.uploadId = res.data.data.uploadId;//文件對應(yīng)的id this.fullPath = res.data.data.fullPath;//上傳合并的地址 this.uploadUrls = res.data.data.uploadUrls;//每個分片對應(yīng)的位置 if (this.uploadUrls && this.uploadUrls.length) { /** * 用于并發(fā)上傳 parallelRun */ // this.waitUpLoad.forEach((item, i) => { // item.formData.append("Upurl", this.uploadUrls[i]); // }); // this.parallelRun(this.waitUpLoad) // return; let i = 0;//第幾個分片對應(yīng)地址 /** * 文件分片合并 */ const complete = () => { Imagecomplete({ bucketName: "static",//MinIO桶名稱 fullPath: this.fullPath,//Imageinit返回的上傳地址 uploadId: this.uploadId,//Imageinit返回的上傳id }).then( (res) => { if (res.data.data) { this.uploading = false; this.$emit("fileinput", { url: "/*minIo桶地址*/" + this.fullPath,//最終文件路徑表單提交用 code: this.hash,//md5值校驗 imageSize: this.imageSize,//文件大小 name: this.file.name,//文件名 }); this.$message({ type: "success", message: "上傳鏡像成功", }); this.$refs.fileValue.value = '' this.uploading = false; } else { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("合并失敗"); } }, (err) => { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("合并失敗"); } ); }; /** * 分片上傳 */ const send = async () => { if (!this.show) { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; return; } /** * 沒有可上傳的請求合并 */ if (i >= this.uploadUrls.length) { // alert('發(fā)送完畢') // 發(fā)送完畢 complete(); return; } if (this.waitNum == 0) return; /** * 通過AXIOS的put將對應(yīng)的分片文件傳到對應(yīng)的桶里 */ try { axios .put( this.uploadUrls[i], this.waitUpLoad[i].formData.get("file") ) .then( (result) => { /*上傳一個分片成功就對應(yīng)減少一個再接著下一個分片上傳 */ this.waitNum--; i++; send(); }, (err) => { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("上傳失敗"); } ); } catch (error) { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("上傳失敗"); } }; send(); // 發(fā)送請求 } } }); } catch (error) { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("上傳失敗"); } }, inputChange() { this.$refs["fileValue"].dispatchEvent(new MouseEvent("click")); }, /** * 用于并發(fā)分片上傳 * requestList 上傳列表 max幾個上傳并發(fā)執(zhí)行 */ async parallelRun(requestList, max = 10) { const requestSliceList = []; for (let i = 0; i < requestList.length; i += max) { requestSliceList.push(requestList.slice(i, i + max)); } for (let i = 0; i < requestSliceList.length; i++) { const group = requestSliceList[i]; console.log(group); try { const res = await Promise.all(group.map(fn => axios.put( fn.formData.get("Upurl"), fn.formData.get("file") ))); res.forEach(item => { this.waitNum-- }) console.log('接口返回值為:', res); if (this.waitNum === 0) { //alert('發(fā)送完畢') // 發(fā)送完畢 this.complete(); return; } // const res = await Promise.all(group.map(fn => fn)); } catch (error) { console.error(error); } } }, complete() { Imagecomplete({ bucketName: "static",//對應(yīng)的桶 fullPath: this.fullPath,//桶的地址 uploadId: this.uploadId,//桶的id }).then( (res) => { if (res.data.data) { this.uploading = false; this.$emit("fileinput", { url: "/*minIo桶地址*/" + this.fullPath,//'https://asgad/fileinput'+'/1000/20240701/xxx.zip' code: this.hash,//文件MD5值 imageSize: this.imageSize,//文件大小 }); this.$message({ type: "success", message: "上傳鏡像成功", }); this.$refs.fileValue.value = '' this.uploading = false; } else { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("合并失敗"); } }, (err) => { this.file = null; this.$refs.fileValue.value = '' this.uploading = false; this.$message.error("合并失敗"); } ); }, /** * 獲取大文件的MD5數(shù)值 * @param {*} file 文件 * @param {*} n 分片大小單位M */ computeFileMD5(file, n = 50 * 1024 * 1024) { //("開始計算...", file); return new Promise((resolve, reject) => { let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; let chunkSize = n; // 默認按照一片 50MB 分片 let chunks = Math.ceil(file.size / chunkSize); // 片數(shù) let currentChunk = 0; let spark = new SparkMD5.ArrayBuffer(); let fileReader = new FileReader(); let that = this; fileReader.onload = function (e) { //console.log("read chunk nr", currentChunk + 1, "of", chunks); spark.append(e.target.result); currentChunk++; // console.log("執(zhí)行進度:" + (currentChunk / chunks) * 100 + "%"); if (currentChunk < chunks && that.show) { loadNext(); } else { // console.log("finished loading"); let md5 = spark.end(); //最終md5值 spark.destroy(); //釋放緩存 if (currentChunk === chunks) { resolve(md5); } else { reject(e); } } }; fileReader.onerror = function (e) { reject(e); }; function loadNext() { let start = currentChunk * chunkSize; let end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }); }, }, }; </script>
頁面樣式
自行修改
<!-- 當前組件頁面樣式定義 --> <style lang="scss" scoped> .file-choose-btn { overflow: hidden; position: relative; input { position: absolute; font-size: 100px; right: 0; top: 0; opacity: 0; cursor: pointer; } } .tips { margin-top: 30px; font-size: 14px; font-weight: 400; color: #606266; } .file-list { margin-top: 10px; } .list-item { display: block; margin-right: 10px; color: #606266; line-height: 25px; margin-bottom: 5px; width: 90%; .percentage { float: right; } } .list-enter-active, .list-leave-active { transition: all 1s; } .list-enter, .list-leave-to /* .list-leave-active for below version 2.1.8 */ { opacity: 0; transform: translateY(-30px); } </style>
總結(jié)
到此這篇關(guān)于前端大文件分片MinIO上傳的文章就介紹到這了,更多相關(guān)前端大文件分片MinIO上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子
這篇文章主要介紹了JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子,這是一種比較高級的應(yīng)用,需要的朋友可以參考下2014-05-05JAVASCRIPT模式窗口中下載文件無法接收iframe的流
模式窗口中下載文件,有時在下載時發(fā)現(xiàn)服務(wù)器無法接收iframe的流,因為在模式窗口中沒有觸發(fā)iframe的src重新定向事件2013-10-10