前端大文件切片上传,断点续传、秒传等解决方案,vue中使用实例
先看逻辑
如何切片?如何获取文件唯一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/
欢迎入群一起讨论
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端