首页 > 其他分享 >前端大文件切片上传,断点续传、秒传等解决方案,vue中使用实例

前端大文件切片上传,断点续传、秒传等解决方案,vue中使用实例

时间:2023-11-01 09:44:25浏览次数:33  
标签:断点续传 vue const res fileList 切片 file 上传

先看逻辑

如何切片?如何获取文件唯一hash?与后端交互获取文件上传的状态,用于判断情况,是秒传还是续传?上传切片文件,判断失败文件重新执行?全部上传完成通知后端?

1、先上全部代码,后面第2部分解析、第3部分vue中使用
相关依赖
spark-md5主要用于拿取文件的md5
mitt 发布订阅

import SparkMD5 from 'spark-md5'
import { getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'
import mitt from '@/utils/mitt'

class UpLoadVideo {
/**
* file 流文件
* sectionSize 切片大小(MB) 默认 5MB
* failCount 失败重试次数 默认 3次
* concurrencyMax 最大并发量
* @param {*} params
*/
constructor(params) {
// file流
this.file = params.file
this.sectionSize = params.sectionSize || 5
this.failCount = params.failCount || 3
this.concurrencyMax = params.concurrencyMax || 1
// 是否停止上传
this.isStop = false
this._count = 0
// 每个文件在上传接口检查前都会返回的上传id md5一样的话 返回的 id都是一样的
this.uploadId = ''
// md5 情况
this.md5 = {
value: '',
progress: 0
}
// 上传进度
this.upProgress = 0
// 0 未开始 1 切片中 2 切片完成 3 开始上传中 4 全部上传完成 5 上传失败 前端定义的状态
this.status = 0
// 发布订阅
this.mitt = mitt
// 切片list
this.fileList = []
// 最开始的切片数量
this.multipartCount = 0
// 视频时长(毫秒)本地获取
this.duration = 0
this.upVideo()
}
/**
* 获取文件md5以及切片
* 以及检查md5的状态 是否已经有上传 或者 上传到一半
* @returns
*/
async upVideo() {
const file = this.file
this.mitt.emit('currentFunc', { msg: '正在检查文件信息' })
const md5 = await this.getMd5(2)
const duration = await this.getDuration(file)
this.duration = duration
const size = 1024 * 1024 * this.sectionSize // 切片大小
const fileList = []
let index = 0 // 切片序号
for (let cur = 0; cur < file.size; cur += size) {
const sectionFile = file.slice(cur, cur + size)
const partIdx = ++index
fileList.push({
partIdx,
multipartName: file.name + '_' + partIdx,
file: sectionFile,
size: sectionFile.size
})
}
this.multipartCount = index
const res = await this.getDetail({
MD5: md5,
Size: file.size,
FileName: file.name,
MultipartCount: fileList.length
})
// 切片数组
const newList = []
// 根据接口返回的数据出来 partIdxList里面表示那些索引表示已上传 筛选出未上传的
if (res.partIdxList && res.partIdxList.length) {
fileList.forEach(item => {
if (!res.partIdxList.includes(item.partIdx)) {
newList.push(item)
}
})
}
this.uploadId = res.uploadId
// state 2 已上传
if (res.state === 2) {
this.status = 4
this.upEmit('currentFunc', { msg: '上传完成', ...res })
return
}
// state 1 上传中 并且 切片长度等于已上传序号列表的长度
if (res.state === 1 && fileList.length === res.partIdxList.length) {
this.getCompleteMultipartUpload(false)
return
}
this.fileList = newList.length ? newList : fileList
this.upSection()
}
// 上传前的查询
async getDetail(data) {
return new Promise((resolve, reject) => {
getStartMultipartUpload(data).then(res => {
resolve(res.data)
})
})
}
/**
* 是否检查
* @param {*} is
*/
async getCompleteMultipartUpload(is = false) {
const formData = new FormData()
formData.append('UploadId', this.uploadId)
postCompleteMultipartUpload(formData).then(res => {
this.upProgress = 100
this.status = 4
this.upEmit('currentFunc', { msg: '上传完成', ...res.data })
})
}
async upSection(fileList) {
if (this._count === this.failCount) {
this.upProgress = 0
this.mitt.emit('currentFunc', { msg: '上传失败,请重新上传试试,会保留您此次的上传进度,下次上传将会加速上传' })
return
}
fileList = fileList || this.fileList
console.time()
if (fileList.length === 0) {
console.timeEnd()
this.getCompleteMultipartUpload(true)
return
}
const pool = []// 并发池
const max = this.concurrencyMax // 最大并发量
let finish = 0// 完成的数量
const failList = []// 失败的列表
const upProgress = this.upProgress
if (!upProgress) {
// 延时1秒执行 并且完成的数量的大于0
setTimeout(() => {
if (!finish) {
this.mitt.emit('currentFunc', { msg: '准备上传...' })
}
}, 1000)
}
this.status = 3
for (let i = 0; i < fileList.length; i++) {
const item = fileList[i]
// 调用接口,上传切片
const task = this.apiFun(item).then(res => {
const progress = (100 / fileList.length * finish)
this.upProgress = Math.floor(progress * 100) / 100
this.upEmit('currentFunc', { msg: '正在上传文件' + this.upProgress + '%' })
// 请求结束后将该Promise任务从并发池中移除
const index = pool.findIndex(t => t === task)
pool.splice(index)
}).catch(_ => {
// 失败的存入失败数组
failList.push(item)
}).finally(_ => {
finish++
// 所有请求都请求完成后 检查失败的数组
if (finish === fileList.length) {
// 检查失败次数
this._count++
this.upSection(failList)
}
})
pool.push(task)
if (pool.length === max) {
// 结束上传
if (this.isStop) break
// 每当并发池跑完一个任务,就再塞入一个任务
await Promise.allSettled(pool)
}
}
}

// 切片请求上传
apiFun(item) {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('UploadId', this.uploadId)
formData.append('MultipartName', item.multipartName)
formData.append('PartIdx', item.partIdx)
formData.append('Size', item.size)
formData.append('File', item.file)
postUploadMultipartPart(formData).then(res => {
resolve(item.partIdx)
}).catch(err => {
reject(err)
})
})
}
upEmit(event = 'currentFunc', data) {
this.mitt.emit(event, { ...data, md5: this.md5, upProgress: this.upProgress, status: this.status, duration: this.duration })
}
/**
*
* @param {*} computeCount 计算次数,不传默认全部切片计算
* @returns
*/
getMd5(computeCount) {
const that = this
return new Promise((resolve, reject) => {
// 兼容
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
// 切片计算大小
const chunkSize = 2097152 // Read in chunks of 2MB
// 当前file流
const file = that.file
// 当前可以分多少切片
let chunks = Math.ceil(file.size / chunkSize)
// 传入的次数小于 切片次数 就使用传入的
if (computeCount && computeCount < chunks) {
chunks = computeCount
}
let currentChunk = 0
// 计算md5的库方法
const spark = new SparkMD5.ArrayBuffer()
// 创建FileReader
const fileReader = new FileReader()
// 写入切片流
function loadNext() {
const start = currentChunk * chunkSize
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
}
loadNext()
// 写入完成后计算md5
fileReader.onload = function(e) {
spark.append(e.target.result) // Append array buffer
currentChunk++
if (currentChunk < chunks) {
const progress = ((100 / chunks) * currentChunk)
this.status = 1
that.md5 = {
value: '',
progress: Math.floor(progress * 100) / 100
}
that.upEmit('currentFunc', { msg: '正在检查文件信息' + that.md5.progress + '%' })
loadNext()
} else {
const md5 = {
value: spark.end(),
progress: 100
}
that.md5 = md5
this.status = 2
that.upEmit('currentFunc', { msg: '检查文件信息100%' })
resolve(md5.value)
}
}

fileReader.onerror = function() {
that.upEmit('currentFunc', { msg: '检查文件信息失败,请重新试试' })
}
})
}
/**
* 获取视频长度
* @param {*} file
* @returns
*/
getDuration(file) {
return new Promise((resolve, reject) => {
const video = document.createElement('video')
video.preload = 'metadata'
video.src = URL.createObjectURL(file)
video.onloadedmetadata = function() {
window.URL.revokeObjectURL(video.src)
const duration = video.duration
resolve(Math.floor(duration * 1000))
}
})
}

/**
*停止上传
*/
stopUpLoad() {
this.upProgress = 0
this.status = 5
this.isStop = true
this.mitt.emit('currentFunc', { msg: '上传失败,请重新试试' })
this.destroy()
}

destroy() {
this.file = null
this.mitt.clear()
}
}

export default UpLoadVideo


2代码解析(函数)
整个上传用的 阿里oss
后台封装了3个api 第一个是上传前的查询、第二个上传视频分片、第三个完成上传

import { getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'
1
2.1 getMd5用于获取文件的md5,我这边只拿取前几个切片数量的作为全局的md5,因为大文件的情况下,获取文件md5也是耗时的操作
2.2 getDuration获取视频时长的
2.3 upVideo每个切片的大小的处理、上传前的查询,主要处理是否已经上传
2.3.1 切片的大小的处理
const size = 1024 * 1024 * this.sectionSize // 切片大小
const fileList = []
let index = 0 // 切片序号
for (let cur = 0; cur < file.size; cur += size) {
const sectionFile = file.slice(cur, cur + size)
const partIdx = ++index
fileList.push({
partIdx, // 后续用于判断那个切片已经上传
multipartName: file.name + '_' + partIdx,
file: sectionFile,
size: sectionFile.size
})
}

2.3.2 上传前的查询
const res = await this.getDetail({
MD5: md5,
Size: file.size,
FileName: file.name,
MultipartCount: fileList.length
})
// 切片数组
const newList = []
// 根据接口返回的数据出来 partIdxList里面表示那些索引表示已上传 筛选出未上传的
if (res.partIdxList && res.partIdxList.length) {
fileList.forEach(item => {
if (!res.partIdxList.includes(item.partIdx)) {
newList.push(item)
}
})
}
this.uploadId = res.uploadId
// state 2 已上传 直接返回结果
if (res.state === 2) {
this.status = 4
this.upEmit('currentFunc', { msg: '上传完成', ...res })
return
}
// state 1 上传中 并且 切片长度等于已上传序号列表的长度 直接调用完成上传接口
if (res.state === 1 && fileList.length === res.partIdxList.length) {
this.getCompleteMultipartUpload(false)
return
}
// 开始上传
this.fileList = newList.length ? newList : fileList
this.upSection()


2.4 upSection 开始分片上传
if (this._count === this.failCount) {
this.upProgress = 0
this.mitt.emit('currentFunc', { msg: '上传失败,请重新上传试试,会保留您此次的上传进度,下次上传将会加速上传' })
return
}
fileList = fileList || this.fileList
console.time()
// 全部上传完成 调用完成接口
if (fileList.length === 0) {
console.timeEnd()
this.getCompleteMultipartUpload(true)
return
}
const pool = []// 并发池
const max = this.concurrencyMax // 最大并发量
let finish = 0// 完成的数量
const failList = []// 失败的列表
const upProgress = this.upProgress
if (!upProgress) {
// 延时1秒执行 并且完成的数量的大于0
setTimeout(() => {
if (!finish) {
this.mitt.emit('currentFunc', { msg: '准备上传...' })
}
}, 1000)
}
this.status = 3
for (let i = 0; i < fileList.length; i++) {
const item = fileList[i]
// 调用接口,上传切片
const task = this.apiFun(item).then(res => {
const progress = (100 / fileList.length * finish)
this.upProgress = Math.floor(progress * 100) / 100
this.upEmit('currentFunc', { msg: '正在上传文件' + this.upProgress + '%' })
// 请求结束后将该Promise任务从并发池中移除
const index = pool.findIndex(t => t === task)
pool.splice(index)
}).catch(_ => {
// 失败的存入失败数组 每次fileList执行完的话 会执行failList的数组
failList.push(item)
}).finally(_ => {
finish++
// 所有请求都请求完成后 检查失败的数组
if (finish === fileList.length) {
// 检查失败次数
this._count++
this.upSection(failList)
}
})
pool.push(task)
if (pool.length === max) {
// 结束上传
if (this.isStop) break
// 每当并发池跑完一个任务,就再塞入一个任务
await Promise.allSettled(pool)
}
}


3、vue中使用封装好的js
vue文件中引入

import UpLoadVideo from '@/utils/uploadVideo'

data(){
return {
uploadVideo: null
}
}
// fileBase 就是 流文件
let newUploadVideo = new UpLoadVideo({
file: fileBase
})
newUploadVideo.mitt.on('currentFunc', data => {
this.loadingText = data.msg // 一些上传中的提示
this.upProgress = data.upProgress // 上传进度
// status是自定义的 state是后台返回的
if (data.status === 4 || data.state === 2) {
console.log('%c [ data 中数据就是接口返回的或者封装js里返回的 ]', 'font-size:13px; background:pink; color:#bf2c9f;', data)
this.upProgress = 0
this.loadingText = ''
newUploadVideo.destroy()
newUploadVideo = null
this.uploadVideo = null
}
})
this.uploadVideo = newUploadVideo

参考文章:http://blog.ncmem.com/wordpress/2023/11/01/%e5%89%8d%e7%ab%af%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%87%e7%89%87%e4%b8%8a%e4%bc%a0%ef%bc%8c%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e3%80%81%e7%a7%92%e4%bc%a0%e7%ad%89%e8%a7%a3%e5%86%b3%e6%96%b9%e6%a1%88/

欢迎入群一起讨论

 

 

标签:断点续传,vue,const,res,fileList,切片,file,上传
From: https://www.cnblogs.com/songsu/p/17802350.html

相关文章

  • [Vue]什么是组件化?
    组件的定义:实现应用中局部功能代码和资源的集合。   ......
  • App开发:Vue Native vs React Native
    随着移动应用开发框架数量的增加,2018年Vue Native的引入让开发者有能力使用Vue.js代码来创建ReactNative移动应用。因为VueNative围绕 React Native包装了Vue.js语法,所以框架非常相似。例如,两者都是跨平台框架,这意味着相同的代码库可以编译为适用于Android和iOS环......
  • 关于 vue 虚拟dom 的渲染机制的一些思考
    1.虚拟dom的渲染过程2.vue3中nexttick的作用 1.虚拟dom的渲染机制我们在template中写的div和其他的标签。不会被vue当作是最终渲染的dom,vue会将我们写入的标签转化为对象,通过diff算法,将其构造成一个虚拟树每个树都有一个对应的key,这个key作为不同阶段的标......
  • Vue2 基础入门
    1Vue1.2第一个Vue实例(HelloVue)1.3插值表达式{作用:利用表达式进行插值,渲染到页面中表达式:是可以被求值的代码,JS引擎会讲其计算出一个结果以下的情况都是表达式:money+100money-100money*10money/10price>=100?'真贵':'还行'obj.namearr[0]fn()ob......
  • 如何采用VuePress构建文档网站
    作者:倾城博客:https://www.codingbrick.com寄语:当你意识到面子不重要时,你才算个真正的成年人。在建设博客的初期,我采用GitBook构建了编码专家的专栏系统。GitBook是基于Node.js的静态页构建组件,可以将Markdown文档渲染为静态页,支持插件扩展,使用非常简单。由于它不支持深度的......
  • Mac 中安装 vue 脚手架后报错 vue: command not found
    解决方案安装node、npm查看npm全局安装位置npmroot-g正确位置/usr/local/lib/node_modules修改位置npmconfigsetprefix/usr/local重新安装脚手架sudonpminstall-g@vue/cli查看vue版本vue-v......
  • 使用yarn build 打包vue项目时静态文件或图片未打包成功
    解决Vue项目使用yarnbuild打包时静态文件或图片未打包成功的问题1.检查vue.config.js文件首先,我们需要检查项目根目录下的vue.config.js文件,该文件用于配置Vue项目的打包和构建选项。在这个文件中,我们需要确认是否正确地配置了打包输出目录和文件规则。可以检查以下几个设置项:mod......
  • java实现文件夹上传功能实例代码(SpringBoot框架)
    前言有时我们后台管理等服务可能会有这样一个简单需求,就是根据文件夹将整个文件夹下的所有资源都上传到我们的服务器上,本人也是搜索了大量资料,最终以最简单便捷的方式实现该功能,具体操作步骤如下一、前端如何设置上传组件并将资源上传到后台服务这里的项目框架为若依VUE版本......
  • vue绑定事件
    <!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0"><title>Document</title>&l......
  • java 上传大文件
    Java上传大文件实现方法简介在开发过程中,我们经常会遇到需要上传大文件的需求。本文将教会你如何用Java实现上传大文件的功能。整体流程首先,让我们来看一下实现上传大文件的整体流程。下面是一个流程表格:步骤 描述1 创建一个文件上传表单2 在后端编写一个接收上传文件的控制器3 ......