当前位置:   article > 正文

vue3+element 分片上传 分片下载功能实现_vue 分片下载

vue 分片下载

思路:分片上传是把一个大文件切割若干等份,前端循环调用上传接口进行上传。分片下载也是一样的道理,前端调用接口拿到文件总大小,计算分割成多少份,循环调用下载接口获取每一段的文件流,获取全部文件片段,进行合并下载。

一、安装依赖

用于获取文件的唯一标识,后端会根据此标识判断是否传过这个文件,传过的话就直接返回文件路径,提示上传成功

npm install spark-md5
  • 1

方法封装

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)
    }
  })
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
二、分片上传
 <el-upload :show-file-list="false" class="upload-demo" :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>
  • 1
  • 2
  • 3

上传的逻辑

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

效果图如下
在这里插入图片描述

三、分片下载
(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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
(2)思路:

1、前端第一次调用接口,请求头默认传 Range:bytes=0-chunkSize ,第一段的文件大小。接口会在响应头返回Content-Range: bytes 0-5242880/534107865,534107865即为总文件大小,需要根据这个总文件大小以及每片大小chunkSize ,计算分为多少段下载。
后端参考代码:https://www.jianshu.com/p/e8dee3dbc409

请求头

第一次Range: bytes=0-5242880,第二次请求Range: bytes=5242880-10485760,依次递增
在这里插入图片描述

响应头

Content-Disposition用于获取文件名称;Content-Range获取总文件大小

在这里插入图片描述
2、根据总文件大小以及每片大小chunkSize计算合并成数组uploadRange。格式如下
在这里插入图片描述
3、第一次调用已经传了Range: bytes=0-5242880,所以循环是从数组第2位开始调用下载接口

(3)代码
<g-button style="margin-top: 20px" type="primary" :loading="downloadObj.downloading" @click="download"> {{ downloadObj.downloading ? '下载中...' : '下载文件' }}</g-button>
    <span> 下载进度({{ downloadObj.percentage }}%)</span>
  • 1
  • 2
//下载逻辑
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)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
四、上传下载完整代码
<template>
  <div class="fsc-slice-upload">
    <div class="upload">
      <el-upload :show-file-list="false" class="upload-demo" :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" class="upload-percent">
      <span class="file-name">{{ modelObj.fileName }}</span>
      <span>{{ modelObj.percentage == 100 ? '上传完成' : '上传中' }}{{ modelObj.percentage }}%</span>
      <!-- 使用进度条 -->
      <!-- <el-progress :stroke-width="10" :percentage="modelObj.percentage" /> -->
    </div>

    <g-button style="margin-top: 20px" 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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240

参考文章:
https://blog.csdn.net/m0_51431448/article/details/127953473
https://www.jianshu.com/p/64694675ca95

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/66620
推荐阅读
相关标签
  

闽ICP备14008679号