vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
思路:分片上傳是把一個大文件切割若干等份,前端循環(huán)調(diào)用上傳接口進行上傳。分片下載也是一樣的道理,前端調(diào)用接口拿到文件總大小,計算分割成多少份,循環(huán)調(diào)用下載接口獲取每一段的文件流,獲取全部文件片段,進行合并下載。
一、安裝依賴
用于獲取文件的唯一標識,后端會根據(jù)此標識判斷是否傳過這個文件,傳過的話就直接返回文件路徑,提示上傳成功
npm install spark-md5
方法封裝
import SparkMD5 from 'spark-md5'
// 獲取文件的唯一MD5標識碼
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,
})
// 上傳之后重新點擊上傳
const handleExceed = uploadFile => {
modelObj.fileList = {}
handleChange({ raw: uploadFile[0] })
}
//后端接口
const { upLoadBypiece, downLoadbyPiece } = api
// 文件上傳 選擇文件時觸發(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唯一標識碼
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
// 文件分片儲存
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)
}
// 保存文件片段到后臺
const saveFileChunk = async (chunkList, fileMd5, fileName) => {
for (let i = 0; i < chunkList.length; i++) {
let formData = new FormData()
formData.append('filePath', props.filePath) // minio存儲的路徑
formData.append('chunk', i) // 當前片段的索引
formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字節(jié)進行分片的,這里是5M)
formData.append('chunks', chunkList.length) // 共有多少分片
formData.append('chunkFile', chunkList[i]) // 當前分片的文件流
formData.append('md5', fileMd5) // 整個文件的MD5唯一標識碼,不是分片
formData.append('fileName', fileName) // 文件的名稱
formData.append('size', chunkList[i].size) // 當前切片的大小(最后一片不一定是5M)
try {
const data = await upLoadBypiece(formData)
//計算當前上傳進度百分比,展示進度條
modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100)
//成功的時候接口會返回文件的相關信息,當有data.fileName,說明上傳成功了
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)
//存儲每一片文件流
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)用接口,請求頭默認傳 Range:bytes=0-chunkSize ,第一段的文件大小。接口會在響應頭返回Content-Range: bytes 0-5242880/534107865,534107865即為總文件大小,需要根據(jù)這個總文件大小以及每片大小chunkSize ,計算分為多少段下載。
請求頭
第一次Range: bytes=0-5242880,第二次請求Range: bytes=5242880-10485760,依次遞增
響應頭
Content-Disposition用于獲取文件名稱;Content-Range獲取總文件大小
- 如果network有Content-Disposition,Content-Range;但是前端取不到值,參考 vue
axios無法獲取響應頭Content-Disposition字段

2、根據(jù)總文件大小以及每片大小chunkSize計算合并成數(shù)組uploadRange。格式如下
3、第一次調(diào)用已經(jīng)傳了Range: bytes=0-5242880,所以循環(huán)是從數(shù)組第2位開始調(diào)用下載接口
(3)代碼
<g-button type="primary" :loading="downloadObj.downloading" @click="download"> {{ downloadObj.downloading ? '下載中...' : '下載文件' }}</g-button>
<span> 下載進度({{ 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 //下載進度
const params = {
md5: '73333a4795dfdfgv266454bbbgfdge41f',
}
const chunkSize = 5 * 1024 * 1024
//第一次調(diào)接口獲取到響應頭的content-range,文件總大小,用于計算下載切割
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])
//存儲每片文件流
downloadObj.fileBlob.push(data.data)
//獲取文件名稱
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,計算分為多少都段下載
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)
//計算下載進度
downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100)
emit('getDownloadpercent', downloadObj.percentage)
//存儲每一片文件流
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>
<!-- 使用進度條 -->
<!-- <el-progress :stroke-width="10" :percentage="modelObj.percentage" /> -->
</div>
<g-button type="primary" :loading="downloadObj.downloading" @click="download"> {{ downloadObj.downloading ? '下載中...' : '下載文件' }}</g-button>
<span> 下載進度({{ 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存儲的路徑
filePath: {
type: String,
default: 'data/data',
},
//是否展示進度條
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
// 上傳之后重新點擊上傳
const handleExceed = uploadFile => {
modelObj.fileList = {}
handleChange({ raw: uploadFile[0] })
}
// 文件上傳 選擇文件時觸發(fā)(:on-change事件)
const handleChange = async (uploadFile, uploadFiles) => {
modelObj.percentage = 0
// 文件信息
let fileRaw = uploadFile.raw
modelObj.fileName = fileRaw.name
modelObj.loading = true
// 獲取 文件的 MD5唯一標識碼
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
// 文件分片儲存
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)
}
// 保存文件片段到后臺
const saveFileChunk = async (chunkList, fileMd5, fileName) => {
for (let i = 0; i < chunkList.length; i++) {
let formData = new FormData()
formData.append('filePath', props.filePath) // minio存儲的路徑
formData.append('chunk', i) // 當前片段的索引
formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字節(jié)進行分片的,這里是5M)
formData.append('chunks', chunkList.length) // 共有多少分片
formData.append('chunkFile', chunkList[i]) // 當前分片的文件流
formData.append('md5', fileMd5) // 整個文件的MD5唯一標識碼,不是分片
formData.append('fileName', fileName) // 文件的名稱
formData.append('size', chunkList[i].size) // 當前切片的大小(最后一片不一定是5M)
try {
const data = await upLoadBypiece(formData)
//計算當前上傳進度百分比,展示進度條
modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100)
//成功的時候接口會返回文件的相關信息,當有data.fileName,說明上傳成功了
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 //下載進度
const params = {
md5: '7343784583fsdufhusdfgsudfe8934',
}
const chunkSize = 5 * 1024 * 1024
//第一次調(diào)接口獲取到響應頭的content-range,文件總大小,用于計算下載切割
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])
//存儲每片文件流
downloadObj.fileBlob.push(data.data)
//獲取文件名稱
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,計算分為多少都段下載
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)傳過了第一組,從第二位開始
if (index > 0) {
const config = {
headers: {
Range: `bytes=${uploadRange[index]}`,
},
}
const data = await downLoadbyPiece(params, config)
//計算下載進度
downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100)
emit('getDownloadpercent', downloadObj.percentage)
//存儲每一片文件流
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>
參考:
相關文章
vxe-table表格組件,編輯單元格詳解(el-table組件同樣適用)
這篇文章主要介紹了vxe-table表格組件,編輯單元格詳解(el-table組件同樣適用),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
vue watch深度監(jiān)聽對象實現(xiàn)數(shù)據(jù)聯(lián)動效果
這篇文章主要介紹了vue watch深度監(jiān)聽對象實現(xiàn)數(shù)據(jù)聯(lián)動的方法,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08
Vue.js實現(xiàn)數(shù)據(jù)雙向綁定的代碼示例
在我們使用vue的時候,當數(shù)據(jù)發(fā)生了改變,界面也會跟著更新,但這并不是理所當然的,我們修改數(shù)據(jù)的時候vue是如何監(jiān)聽數(shù)據(jù)的改變以及當數(shù)據(jù)發(fā)生改變的時候vue如何讓界面刷新的,所以本文就給大家講講Vue.js 數(shù)據(jù)雙向綁定是如何實現(xiàn)的2023-07-07
一文教你徹底解決Vue3中的ResizeObserver警告
這篇文章主要為大家詳細介紹了介紹了Vue3中出現(xiàn)ResizeObserver loop completed with undelivered notifications錯誤的相關解決方法,有需要的小伙伴可以跟隨小編一起學習一下2025-05-05
Vue3中v-model實現(xiàn)雙向數(shù)據(jù)綁定的全面指南
在Vue開發(fā)中,v-model是實現(xiàn)表單輸入和應用狀態(tài)之間雙向綁定的關鍵指令,Vue 3對v-model進行了重大改進,使其更加靈活和強大,本文將深入探討 Vue 3中v-model的工作原理、新特性以及最佳實踐,需要的朋友可以參考下2025-07-07

