vue3+element 分片上傳與分片下載功能實(shí)現(xiàn)方法詳解
思路:分片上傳是把一個(gè)大文件切割若干等份,前端循環(huán)調(diào)用上傳接口進(jìn)行上傳。分片下載也是一樣的道理,前端調(diào)用接口拿到文件總大小,計(jì)算分割成多少份,循環(huán)調(diào)用下載接口獲取每一段的文件流,獲取全部文件片段,進(jìn)行合并下載。
一、安裝依賴(lài)
用于獲取文件的唯一標(biāo)識(shí),后端會(huì)根據(jù)此標(biāo)識(shí)判斷是否傳過(guò)這個(gè)文件,傳過(guò)的話就直接返回文件路徑,提示上傳成功
npm install spark-md5
方法封裝
import SparkMD5 from 'spark-md5' // 獲取文件的唯一MD5標(biāo)識(shí)碼 export function getFileMd5(file) { return new Promise((resolve, reject) => { const fileReader = new FileReader() const spark = new SparkMD5.ArrayBuffer() fileReader.readAsArrayBuffer(file) fileReader.onload = e => { spark.append(e.target.result) let md5 = spark.end() resolve(md5) } }) }
二、分片上傳
<el-upload :show-file-list="false" :auto-upload="false" :limit="1" :on-change="handleChange" :on-exceed="handleExceed" :multiple="false"> <el-button type="primary" :loading="modelObj.loading">{{ modelObj.loading ? '上傳中...' : buttonTitle_ }}</g-button> </el-upload>
上傳的邏輯
import { getFileMd5 } from './method' import { computed, reactive} from 'vue' const modelObj = reactive({ fileList: {}, loading: false, percentage: 0, }) // 上傳之后重新點(diǎn)擊上傳 const handleExceed = uploadFile => { modelObj.fileList = {} handleChange({ raw: uploadFile[0] }) } //后端接口 const { upLoadBypiece, downLoadbyPiece } = api // 文件上傳 選擇文件時(shí)觸發(fā)(:on-change事件) const handleChange = async (uploadFile, uploadFiles) => { modelObj.percentage = 0 // 文件信息 let fileRaw = uploadFile.raw modelObj.fileName = fileRaw.name console.log(fileRaw, 'fileRaw') modelObj.loading = true // 獲取 文件的 MD5唯一標(biāo)識(shí)碼 let fileMd5 = null try { fileMd5 = await getFileMd5(fileRaw) } catch (e) { console.error('[error]', e) } if (!fileMd5) return // 每片的大小為 5M 可調(diào)整 const chunkSize = 5 * 1024 * 1024 // 文件分片儲(chǔ)存 let chunkList = [] function chunkPush(page = 1) { chunkList.push(fileRaw.slice((page - 1) * chunkSize, page * chunkSize)) if (page * chunkSize < fileRaw.size) { chunkPush(page + 1) } } chunkPush() saveFileChunk(chunkList, fileMd5, fileRaw.name) } // 保存文件片段到后臺(tái) const saveFileChunk = async (chunkList, fileMd5, fileName) => { for (let i = 0; i < chunkList.length; i++) { let formData = new FormData() formData.append('filePath', props.filePath) // minio存儲(chǔ)的路徑 formData.append('chunk', i) // 當(dāng)前片段的索引 formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字節(jié)進(jìn)行分片的,這里是5M) formData.append('chunks', chunkList.length) // 共有多少分片 formData.append('chunkFile', chunkList[i]) // 當(dāng)前分片的文件流 formData.append('md5', fileMd5) // 整個(gè)文件的MD5唯一標(biāo)識(shí)碼,不是分片 formData.append('fileName', fileName) // 文件的名稱(chēng) formData.append('size', chunkList[i].size) // 當(dāng)前切片的大?。ㄗ詈笠黄灰欢ㄊ?M) try { const data = await upLoadBypiece(formData) //計(jì)算當(dāng)前上傳進(jìn)度百分比,展示進(jìn)度條 modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100) //成功的時(shí)候接口會(huì)返回文件的相關(guān)信息,當(dāng)有data.fileName,說(shuō)明上傳成功了 if (data.fileName) { modelObj.percentage = 100 modelObj.loading = false modelObj.fileList = data emit('getFile', modelObj.fileList) console.log(modelObj.fileList, 'modelObj.fileList') message.success(`上傳成功`) return } } catch (e) { modelObj.loading = false } } }
效果圖如下
三、分片下載
(1)分片下載合并核心偽代碼
let fileBlob=[] for (let index = 0; index < 5; index++) { const params={} const config={} const data = await downLoadbyPiece(params, config) //存儲(chǔ)每一片文件流 fileBlob.push(data.data) } //合并 const blob = new Blob(fileBlob, { type:fileBlob[0].type, }) //下載 const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = fileName link.click() window.URL.revokeObjectURL(link.href)
(2)思路:
1、前端第一次調(diào)用接口,請(qǐng)求頭默認(rèn)傳 Range:bytes=0-chunkSize ,第一段的文件大小。接口會(huì)在響應(yīng)頭返回Content-Range: bytes 0-5242880/534107865,534107865即為總文件大小,需要根據(jù)這個(gè)總文件大小以及每片大小chunkSize ,計(jì)算分為多少段下載。
請(qǐng)求頭
第一次Range: bytes=0-5242880,第二次請(qǐng)求Range: bytes=5242880-10485760,依次遞增
響應(yīng)頭
Content-Disposition用于獲取文件名稱(chēng);Content-Range獲取總文件大小
- 如果network有Content-Disposition,Content-Range;但是前端取不到值,參考 vue
axios無(wú)法獲取響應(yīng)頭Content-Disposition字段
2、根據(jù)總文件大小以及每片大小chunkSize計(jì)算合并成數(shù)組uploadRange。格式如下
3、第一次調(diào)用已經(jīng)傳了Range: bytes=0-5242880,所以循環(huán)是從數(shù)組第2位開(kāi)始調(diào)用下載接口
(3)代碼
<g-button type="primary" :loading="downloadObj.downloading" @click="download"> {{ downloadObj.downloading ? '下載中...' : '下載文件' }}</g-button> <span> 下載進(jìn)度({{ downloadObj.percentage }}%)</span>
//下載邏輯 const downloadObj = reactive({ fileName: '', downloading: false, range: 0, fileBlob: [], percentage: 0, }) const download = async () => { downloadObj.fileBlob = []//存接口返回的每一段的文件流 downloadObj.downloading = true downloadObj.range = 0 //文件總大小 downloadObj.percentage = 0 //下載進(jìn)度 const params = { md5: '73333a4795dfdfgv266454bbbgfdge41f', } const chunkSize = 5 * 1024 * 1024 //第一次調(diào)接口獲取到響應(yīng)頭的content-range,文件總大小,用于計(jì)算下載切割 const config = { headers: { Range: `bytes=0-${chunkSize}`, }, } const data = await downLoadbyPiece(params, config) //獲取文件總大小 const arr = data.headers['content-range'].split('/') downloadObj.range = Number(arr[1]) //存儲(chǔ)每片文件流 downloadObj.fileBlob.push(data.data) //獲取文件名稱(chēng) let fileName = '' let cd = data.headers['content-disposition'] if (cd) { let index = cd.lastIndexOf('=') fileName = decodeURI(cd.substring(index + 1, cd.length)) } await chunkUpload(params, fileName, chunkSize) } //拿到文件總大小downloadObj.range,計(jì)算分為多少都段下載 const chunkUpload = async (params, fileName, chunkSize) => { //獲取分段下載的數(shù)組 let chunkList = [] function chunkPush(page = 1) { chunkList.push((page - 1) * chunkSize) if (page * chunkSize < downloadObj.range) { chunkPush(page + 1) } } chunkPush() chunkList.push(downloadObj.range) console.log(chunkList, 'chunkList') //分段組合傳參格式處理 0-1024 1024-2048 let uploadRange = [] chunkList.forEach((item, i) => { if (i == chunkList.length - 1) return uploadRange.push(`${chunkList[i]}-${chunkList[i + 1]}`) }) console.log(uploadRange, 'uploadRang') for (let index = 0; index < uploadRange.length; index++) { if (index > 0) { const config = { headers: { Range: `bytes=${uploadRange[index]}`, }, } const data = await downLoadbyPiece(params, config) //計(jì)算下載進(jìn)度 downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100) emit('getDownloadpercent', downloadObj.percentage) //存儲(chǔ)每一片文件流 downloadObj.fileBlob.push(data.data) } } //合并 const blob = new Blob(downloadObj.fileBlob, { type: downloadObj.fileBlob[0].type, }) downloadObj.downloading = false //下載 const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = fileName link.click() window.URL.revokeObjectURL(link.href) }
四、上傳下載完整代碼
<template> <div> <div> <el-upload :show-file-list="false" :auto-upload="false" :limit="1" :on-change="handleChange" :on-exceed="handleExceed" :multiple="false"> <g-button type="primary" :loading="modelObj.loading">{{ modelObj.loading ? '上傳中...' : buttonTitle_ }}</g-button> </el-upload> </div> <div v-show="modelObj.fileName && showPercentage"> <span>{{ modelObj.fileName }}</span> <span>{{ modelObj.percentage == 100 ? '上傳完成' : '上傳中' }}({{ modelObj.percentage }}%)</span> <!-- 使用進(jìn)度條 --> <!-- <el-progress :stroke-width="10" :percentage="modelObj.percentage" /> --> </div> <g-button type="primary" :loading="downloadObj.downloading" @click="download"> {{ downloadObj.downloading ? '下載中...' : '下載文件' }}</g-button> <span> 下載進(jìn)度({{ downloadObj.percentage }}%)</span> </div> </template> <script setup> import { computed, reactive, ref } from 'vue' import { getFileMd5 } from './method' import { useMessage } from 'ui' const message = useMessage() const props = defineProps({ buttonTitle: { type: String, default: '上傳文件', }, //minio存儲(chǔ)的路徑 filePath: { type: String, default: 'data/data', }, //是否展示進(jìn)度條 showPercentage: { type: Boolean, default: true, }, }) const buttonTitle_ = computed(() => props.buttonTitle) const modelObj = reactive({ fileList: {}, loading: false, percentage: 0, }) const emit = defineEmits(['getFile', 'getDownloadpercent']) const { upLoadBypiece, downLoadbyPiece } = api // 上傳之后重新點(diǎn)擊上傳 const handleExceed = uploadFile => { modelObj.fileList = {} handleChange({ raw: uploadFile[0] }) } // 文件上傳 選擇文件時(shí)觸發(fā)(:on-change事件) const handleChange = async (uploadFile, uploadFiles) => { modelObj.percentage = 0 // 文件信息 let fileRaw = uploadFile.raw modelObj.fileName = fileRaw.name modelObj.loading = true // 獲取 文件的 MD5唯一標(biāo)識(shí)碼 let fileMd5 = null try { fileMd5 = await getFileMd5(fileRaw) } catch (e) { console.error('[error]', e) } if (!fileMd5) return // 每片的大小為 5M 可調(diào)整 const chunkSize = 5 * 1024 * 1024 // 文件分片儲(chǔ)存 let chunkList = [] function chunkPush(page = 1) { chunkList.push(fileRaw.slice((page - 1) * chunkSize, page * chunkSize)) if (page * chunkSize < fileRaw.size) { chunkPush(page + 1) } } chunkPush() saveFileChunk(chunkList, fileMd5, fileRaw.name) } // 保存文件片段到后臺(tái) const saveFileChunk = async (chunkList, fileMd5, fileName) => { for (let i = 0; i < chunkList.length; i++) { let formData = new FormData() formData.append('filePath', props.filePath) // minio存儲(chǔ)的路徑 formData.append('chunk', i) // 當(dāng)前片段的索引 formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字節(jié)進(jìn)行分片的,這里是5M) formData.append('chunks', chunkList.length) // 共有多少分片 formData.append('chunkFile', chunkList[i]) // 當(dāng)前分片的文件流 formData.append('md5', fileMd5) // 整個(gè)文件的MD5唯一標(biāo)識(shí)碼,不是分片 formData.append('fileName', fileName) // 文件的名稱(chēng) formData.append('size', chunkList[i].size) // 當(dāng)前切片的大小(最后一片不一定是5M) try { const data = await upLoadBypiece(formData) //計(jì)算當(dāng)前上傳進(jìn)度百分比,展示進(jìn)度條 modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100) //成功的時(shí)候接口會(huì)返回文件的相關(guān)信息,當(dāng)有data.fileName,說(shuō)明上傳成功了 if (data.fileName) { modelObj.percentage = 100 modelObj.loading = false modelObj.fileList = data emit('getFile', modelObj.fileList) console.log(modelObj.fileList, 'modelObj.fileList') message.success(`上傳成功`) return } } catch (e) { modelObj.loading = false } } } //下載邏輯 const downloadObj = reactive({ fileName: '', downloading: false, range: 0, fileBlob: [], percentage: 0, }) const download = async () => { downloadObj.fileBlob = [] downloadObj.downloading = true downloadObj.range = 0 //文件總大小 downloadObj.percentage = 0 //下載進(jìn)度 const params = { md5: '7343784583fsdufhusdfgsudfe8934', } const chunkSize = 5 * 1024 * 1024 //第一次調(diào)接口獲取到響應(yīng)頭的content-range,文件總大小,用于計(jì)算下載切割 const config = { headers: { Range: `bytes=0-${chunkSize}`, }, } const data = await downLoadbyPiece(params, config) //獲取文件總大小 const arr = data.headers['content-range'].split('/') downloadObj.range = Number(arr[1]) //存儲(chǔ)每片文件流 downloadObj.fileBlob.push(data.data) //獲取文件名稱(chēng) let fileName = '' let cd = data.headers['content-disposition'] if (cd) { let index = cd.lastIndexOf('=') fileName = decodeURI(cd.substring(index + 1, cd.length)) } await chunkUpload(params, fileName, chunkSize) } //拿到文件總大小downloadObj.range,計(jì)算分為多少都段下載 const chunkUpload = async (params, fileName, chunkSize) => { //獲取分段下載的數(shù)組 let chunkList = [] function chunkPush(page = 1) { chunkList.push((page - 1) * chunkSize) if (page * chunkSize < downloadObj.range) { chunkPush(page + 1) } } chunkPush() //加上文件大小在最后一位 chunkList.push(downloadObj.range) console.log(chunkList, 'chunkList') //分段組合傳參格式處理 0-1024 1024-2048 let uploadRange = [] chunkList.forEach((item, i) => { if (i == chunkList.length - 1) return uploadRange.push(`${chunkList[i]}-${chunkList[i + 1]}`) }) console.log(uploadRange, 'uploadRang') for (let index = 0; index < uploadRange.length; index++) { //第一次調(diào)接口已經(jīng)傳過(guò)了第一組,從第二位開(kāi)始 if (index > 0) { const config = { headers: { Range: `bytes=${uploadRange[index]}`, }, } const data = await downLoadbyPiece(params, config) //計(jì)算下載進(jìn)度 downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100) emit('getDownloadpercent', downloadObj.percentage) //存儲(chǔ)每一片文件流 downloadObj.fileBlob.push(data.data) } } //合并 const blob = new Blob(downloadObj.fileBlob, { type: downloadObj.fileBlob[0].type, }) downloadObj.downloading = false //下載 const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = fileName link.click() window.URL.revokeObjectURL(link.href) } </script> <style lang="scss"> .fsc-slice-upload { .upload { display: flex; } .upload-percent { margin-top: 10px; display: flex; .file-name { margin-right: 15px; max-width: 200px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .el-progress { flex: 1; } } } </style>
參考:
相關(guān)文章
vue3項(xiàng)目如何國(guó)際化實(shí)戰(zhàn)指南
像很多大型的網(wǎng)址,特別是跨國(guó)際等公司網(wǎng)頁(yè),訪問(wèn)來(lái)自世界各地用戶(hù),所以網(wǎng)頁(yè)的國(guó)際化極其重要的需求,下面這篇文章主要給大家介紹了關(guān)于vue3項(xiàng)目如何國(guó)際化的相關(guān)資料,需要的朋友可以參考下2022-09-09基于vue-simplemde實(shí)現(xiàn)圖片拖拽、粘貼功能
這篇文章主要介紹了基于vue-simplemde實(shí)現(xiàn)圖片拖拽、粘貼功能,需要的朋友可以參考下2018-04-04vue3響應(yīng)式轉(zhuǎn)換常用API操作示例代碼
在Vue 3中,響應(yīng)式系統(tǒng)得到了顯著改善,包括使用Composition API時(shí)更加靈活的狀態(tài)管理,這篇文章主要介紹了vue3響應(yīng)式轉(zhuǎn)換常用API操作示例代碼,需要的朋友可以參考下2024-08-08vue如何動(dòng)態(tài)實(shí)時(shí)的顯示時(shí)間淺析
這篇文章主要給大家介紹了關(guān)于vue如何動(dòng)態(tài)實(shí)時(shí)的顯示時(shí)間,以及vue時(shí)間戳 獲取本地時(shí)間實(shí)時(shí)更新的相關(guān)資料,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05vue項(xiàng)目實(shí)現(xiàn)國(guó)際化的基本思路與詳細(xì)步驟
國(guó)際化是指項(xiàng)目能夠根據(jù)不同國(guó)家的語(yǔ)言進(jìn)行轉(zhuǎn)換,便于不同國(guó)家的用戶(hù)使用,這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目實(shí)現(xiàn)國(guó)際化的基本思路與詳細(xì)步驟,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04vue設(shè)置全局變量5種方法(讓你的數(shù)據(jù)無(wú)處不在)
這篇文章主要給大家介紹了關(guān)于vue設(shè)置全局變量的5種方法,通過(guò)設(shè)置的方法可以讓你的數(shù)據(jù)無(wú)處不在,在項(xiàng)目中經(jīng)常會(huì)復(fù)用一些變量和函數(shù),比如用戶(hù)的登錄token,用戶(hù)信息等,這時(shí)將它們?cè)O(shè)為全局的就顯得很重要了,需要的朋友可以參考下2023-11-11Vue3中是如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式示例詳解
這篇文章主要介紹了Vue3中是如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07