vue3+element 分片上传与分片下载功能实现方法详解
作者:@七叶一枝花
这篇文章主要介绍了vue3+element 分片上传与分片下载功能实现方法,结合实例形式详细分析了vue3+element 分片上传与下载相关实现技巧与操作注意事项,需要的朋友可以参考下
思路:分片上传是把一个大文件切割若干等份,前端循环调用上传接口进行上传。分片下载也是一样的道理,前端调用接口拿到文件总大小,计算分割成多少份,循环调用下载接口获取每一段的文件流,获取全部文件片段,进行合并下载。
一、安装依赖
用于获取文件的唯一标识,后端会根据此标识判断是否传过这个文件,传过的话就直接返回文件路径,提示上传成功
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 // 文件上传 选择文件时触发(: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 可调整 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) // 切片的文件分片大小 (就是以多少字节进行分片的,这里是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、前端第一次调用接口,请求头默认传 Range:bytes=0-chunkSize ,第一段的文件大小。接口会在响应头返回Content-Range: bytes 0-5242880/534107865,534107865即为总文件大小,需要根据这个总文件大小以及每片大小chunkSize ,计算分为多少段下载。
请求头
第一次Range: bytes=0-5242880,第二次请求Range: bytes=5242880-10485760,依次递增
响应头
Content-Disposition用于获取文件名称;Content-Range获取总文件大小
- 如果network有Content-Disposition,Content-Range;但是前端取不到值,参考 vue
axios无法获取响应头Content-Disposition字段
2、根据总文件大小以及每片大小chunkSize计算合并成数组uploadRange。格式如下
3、第一次调用已经传了Range: bytes=0-5242880,所以循环是从数组第2位开始调用下载接口
(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 //第一次调接口获取到响应头的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) => { //获取分段下载的数组 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] }) } // 文件上传 选择文件时触发(: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 可调整 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) // 切片的文件分片大小 (就是以多少字节进行分片的,这里是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 //第一次调接口获取到响应头的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) => { //获取分段下载的数组 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) } </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>
参考:
https://www.jb51.net/article/273534.htm
https://www.jb51.net/article/208589.htm