前端大文件分片MinIO上傳的詳細(xì)代碼
大文件分片上傳是一種將大文件拆分成多個(gè)小文件片段進(jìn)行上傳的技術(shù)。這種方式可以提高上傳效率,減少網(wǎng)絡(luò)傳輸時(shí)間,并且在網(wǎng)絡(luò)不穩(wěn)定或者上傳過程中出現(xiàn)錯(cuò)誤時(shí),可以更容易地恢復(fù)上傳進(jìn)度。
大文件分片上傳的步驟如下:
- 將大文件分成多個(gè)固定大小的片段,通常每個(gè)片段的大小在幾十KB到幾MB之間。
- 逐個(gè)上傳每個(gè)文件片段,可以使用HTTP、FTP等協(xié)議進(jìn)行傳輸。
- 服務(wù)器接收到每個(gè)文件片段后,判斷其MD5值進(jìn)行保存或者合并操作。
- 在上傳過程中,通過MD5值維護(hù)一個(gè)上傳進(jìn)度記錄,標(biāo)記已經(jīng)上傳成功的文件片段,以便在上傳中斷后能夠恢復(fù)上傳進(jìn)度。
- 當(dāng)所有文件片段都上傳完成后,服務(wù)器將文件片段進(jìn)行合并,得到完整的大文件。
大文件分片上傳的好處有:
- 提高上傳速度:將大文件拆分成小片段,可以同時(shí)上傳多個(gè)片段,從而提高上傳速度。
- 斷點(diǎn)續(xù)傳:如果在上傳過程中發(fā)生中斷或者錯(cuò)誤,可以根據(jù)上傳進(jìn)度記錄,只重新上傳丟失或者出錯(cuò)的文件片段,從而減少網(wǎng)絡(luò)傳輸時(shí)間。
- 易于管理:將大文件拆分成小片段,可以更方便地管理和存儲(chǔ),避免了一次性上傳整個(gè)大文件可能導(dǎo)致的內(nèi)存占用問題。
大文件分片上傳技術(shù)已經(jīng)廣泛應(yīng)用于各種云存儲(chǔ)、文件傳輸?shù)阮I(lǐng)域,為用戶提供了更好的上傳體驗(yàn)和效率。
視圖代碼
大文件分片需要讀取時(shí)間所以要給加載狀態(tài),下面例子只適合單文件上傳且?guī)蟼鬟M(jìn)度展示
<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">
請(qǐng)上傳不超過 <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,//檢驗(yàn)是否上傳過用于斷點(diǎn)續(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
* 代碼注釋說明
* 未上傳個(gè)數(shù)
* @Return
*/
waitNum: NaN,
/**
* @Description
* 代碼注釋說明
* 上傳大小限制
* @Return
*/
limitFileSize: false,
/**
* @Description
* 代碼注釋說明
* 進(jìn)度條
* @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)控上傳進(jì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]; // 獲取當(dāng)前文件
this.imageSize = this.ramFilter(fileInput.size);//記錄文件大小
if (!fileInput && !this.show) {
return;
}
const pattern = /[\u4e00-\u9fa5]/;
if (pattern.test(fileInput?.name)) {
this.$message.warning("請(qǐng)不要上傳帶有中文名稱的鏡像文件!");
return;
}
this.file = fileInput; // file 丟全局方便后面用 可以改進(jìn)為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 = []; // 分片存儲(chǔ)的一個(gè)池子 丟全局
this.waitUpLoad = []; // 分片存儲(chǔ)的一個(gè)池子 丟全局
while (end < this.file.size && this.show) {
/**
* @Description
* 代碼注釋說明
* 當(dāng)結(jié)尾數(shù)字大于文件總size的時(shí)候 結(jié)束切片
* @Return
*/
start = i * FileSliceCap; // 計(jì)算每片開始位置
end = (i + 1) * FileSliceCap; // 計(jì)算每片結(jié)束位置
var fileSlice = this.file.slice(start, end); // 開始切 file.slice 為 h5方法 對(duì)文件切片 參數(shù)為 起止字節(jié)數(shù)
const formData = new window.FormData(); // 創(chuàng)建FormData用于存儲(chǔ)傳給后端的信息
// formData.append('fileMd5', this.fileMd5) // 存儲(chǔ)總文件的Md5 讓后端知道自己是誰的切片
formData.append("file", fileSlice); // 當(dāng)前的切片
formData.append("chunkNumber", i); // 當(dāng)前是第幾片
formData.append("fileName", this.file.name); // 當(dāng)前文件的文件名 用于后端文件切片的命名 formData.appen 為 formData對(duì)象添加參數(shù)的方法
this.formDataList.push({ key: i, formData }); // 把當(dāng)前切片信息 自己是第幾片 存入我們方才準(zhǔn)備好的池子
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ā)一個(gè)請(qǐng)求給后端 拉當(dāng)前文件后臺(tái)存儲(chǔ)的切片信息 用于檢測(cè)有多少上傳成功的切片
* fileUrl:有地址就是秒傳因?yàn)橐呀?jīng)存在該文件了
* shardingIndex:返回哪些已經(jīng)上傳用于斷點(diǎn)續(xù)傳
* @Return
*/
let { fileUrl, shardingIndex } = res2.data.data; //檢測(cè)是否上傳過
if (!fileUrl) {
/**
* @Description
* 代碼注釋說明
* 當(dāng)是斷點(diǎn)續(xù)傳時(shí)候
* 記得處理一下當(dāng)前是默認(rèn)全都沒上傳過暫不支持?jǐn)帱c(diǎn)續(xù)傳后端無法返回已上傳數(shù)據(jù)如果你家后端牛一點(diǎn)可以在此處理斷點(diǎn)續(xù)傳
* @Return
*/
this.waitUpLoad = this.formDataList;//當(dāng)前是默認(rèn)全都沒上傳過斷點(diǎn)續(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; // 記錄長(zhǎng)度用于百分比展示
},
(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("請(qǐng)上傳小于100G的文件");
this.file = null;
this.$refs.fileValue.value = ''
this.uploading = false;
}
},
//準(zhǔn)備上傳
getFile() {
/**
* @Description
* 代碼注釋說明
* 確定按鈕
* @Return
*/
if (this.file === null) {
this.$message.error("請(qǐng)先上傳文件");
return;
}
this.percentageFlage = this.percentage == 100;
this.sliceFile(); // 上傳切片
},
async sliceFile() {
/**
* @Description
* 代碼注釋說明
* 如果已上傳文件且生成了文件路徑
* @Return
*/
if (this.fileData.path) {
return;
}
/**
* @Description
* 代碼注釋說明
* 如果是切片已全部上傳 但還未完成合并及移除chunk操作 沒有生成文件路徑時(shí)
* @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時(shí)調(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,//分片多少也就是分了多少個(gè)
};
//根據(jù)分片長(zhǎng)度獲取分片上傳地址以及上傳ID和文件地址
Imageinit(data).then((res) => {
if (res.data.code == 200 && res.data.data) {
this.uploadId = res.data.data.uploadId;//文件對(duì)應(yīng)的id
this.fullPath = res.data.data.fullPath;//上傳合并的地址
this.uploadUrls = res.data.data.uploadUrls;//每個(gè)分片對(duì)應(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;//第幾個(gè)分片對(duì)應(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值校驗(yàn)
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;
}
/**
* 沒有可上傳的請(qǐng)求合并
*/
if (i >= this.uploadUrls.length) {
// alert('發(fā)送完畢')
// 發(fā)送完畢
complete();
return;
}
if (this.waitNum == 0) return;
/**
* 通過AXIOS的put將對(duì)應(yīng)的分片文件傳到對(duì)應(yīng)的桶里
*/
try {
axios
.put(
this.uploadUrls[i],
this.waitUpLoad[i].formData.get("file")
)
.then(
(result) => {
/*上傳一個(gè)分片成功就對(duì)應(yīng)減少一個(gè)再接著下一個(gè)分片上傳
*/
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ā)送請(qǐng)求
}
}
});
} 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幾個(gè)上傳并發(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",//對(duì)應(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) {
//("開始計(jì)算...", file);
return new Promise((resolve, reject) => {
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let chunkSize = n; // 默認(rè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í)行進(jìn)度:" + (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>頁面樣式
自行修改
<!-- 當(dāng)前組件頁面樣式定義 -->
<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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
懶就要懶到底——鼠標(biāo)自動(dòng)點(diǎn)擊(含時(shí)間判斷)
懶就要懶到底——鼠標(biāo)自動(dòng)點(diǎn)擊(含時(shí)間判斷)...2007-02-02
JavaScript碰撞檢測(cè)原理及其實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了JavaScript碰撞檢測(cè)原理及其實(shí)現(xiàn)代碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子
這篇文章主要介紹了JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子,這是一種比較高級(jí)的應(yīng)用,需要的朋友可以參考下2014-05-05
JAVASCRIPT模式窗口中下載文件無法接收iframe的流
模式窗口中下載文件,有時(shí)在下載時(shí)發(fā)現(xiàn)服務(wù)器無法接收iframe的流,因?yàn)樵谀J酱翱谥袥]有觸發(fā)iframe的src重新定向事件2013-10-10

