最近在做一个项目的重构,其中有大文件上传的功能,由于项目是几年前,代码没有前后分离,用的是 jQuery + webuploader 库做的,但实际上只是实现了大文件切片上传,并没有切片并发、秒传及断点续传功能,后端也不支持,且 webuploader 库已经不再维护了,故决定自己实现一个最简单的大文件切片上传功能。
可上传本地录音录像,支持上传的
音频格式为:mp3、m4a、aac
视频格式为:mp4、m4v
上传录音录像
正在上传:{{ fileData.name }}
取消上传
const beforeUpload = (file: File) => {
const mimeTypes = ['audio/mpeg', 'audio/x-m4a', 'audio/aac', 'video/mp4', 'video/x-m4v']
if (!mimeTypes.includes(file.type)) {
ElMessage({
type: 'error',
message: '只能上传 MP3、M4A、AAC、MP4、M4V 格式的文件',
duration: 6000
})
return false
}
if (file.size / 1024 / 1024 / 1024 > 1.5) {
ElMessage.error('文件大小不能超过 1.5G')
ElMessage({
type: 'error',
message: '文件大小不能超过 1.5G',
duration: 6000
})
return false
}
return true
}
const chunkSize = 1 * 1024 * 1024 // 切片大小
const upload = async (file: { file: File }) => {
const fileObj = file.file
const nameList = fileObj.name.split('.')
fileData.value.name = fileObj.name
fileData.value.size = fileObj.size
fileData.value.type = fileObj.type
fileData.value.suffix = nameList[nameList.length - 1]
if (chunkSize > fileData.value.size) { // 文件大小小于切片大小,直接上传
disabled.value = true
axios
.post('upload', fileObj) // 调用后端上传文件接口
.then((res) => {
ElMessageBox({ message: `${fileData.value.name}上传成功`, title: '提示' })
updateUrl(res.data) // 调用后端保存上传文件路径接口
})
.catch(() => ElMessageBox({ message: `${fileData.value.name}上传失败`, title: '提示' })) // 上传失败弹框
.finally(() => (disabled.value = false))
return
}
batchUpload(fileObj) // 大文件切片上传
}
// 重构项目没有断点续传等功能,故不需要做hash计算,只需要保证唯一即可,后端会拿这个值新建文件夹保存切片
let counter = 0
const getFileMd5 = () => {
let guid = (+new Date()).toString(32)
for (let i = 0; i < 5; i++) {
guid += Math.floor(Math.random() * 65535).toString(32)
}
return 'wu_' + guid + (counter++).toString(32)
}
const percentage = ref(0)
const dialogVisible = ref(false)
const cancelUpload = ref(false)
const batchUpload = async (fileObj: File) => {
percentage.value = 0 // 每次上传文件前清空进度条
dialogVisible.value = true // 显示上传进度
cancelUpload.value = false // 每次上传文件前将取消上传标识置为 false
const chunkCount = Math.ceil(fileData.value.size / chunkSize) // 切片数量
fileData.value.md5 = getFileMd5() // 文件唯一标识
for (let i = 0; i < chunkCount; i++) {
if (cancelUpload.value) return // 若已经取消上传,则不再上传切片
const res = await uploadChunkFile(i, fileObj) // 上传切片
if (res.code !== 0) { // 切片上传失败
dialogVisible.value = false
ElMessageBox({ message: `${fileData.value.name}上传失败`, title: '提示' })
return
}
if (i === chunkCount - 1) { // 最后一片切片上传成功
setTimeout(() => { // 延迟关闭上传进度框用户体验会更好
dialogVisible.value = false
ElMessageBox({ message: `${fileData.value.name}上传成功`, title: '提示' })
axios.post('mergeUpload', { folder: fileData.value.md5 }) // 调用后端合并切片接口,参数需要与后端对齐
.then((res) => updateUrl(res.data)) // 调用后端保存上传文件路径接口
}, 500)
}
}
}
let controller: AbortController | null = null // 当前切片上传 AbortController
const uploadChunkFile = async (i: number, fileObj: File) => {
const start = i * chunkSize // 切片开始位置
const end = Math.min(fileData.value.size, start + chunkSize) // 切片结束位置
const chunkFile = fileObj.slice(start, end) // 切片文件
const formData = new FormData() // formData 参数需要与后端对齐
formData.append('fileName', fileData.value.name)
formData.append('folder', fileData.value.md5)
formData.append('file', chunkFile, String(i + 1)) // 必传字段;若第三个参数不传,切片 filename 默认是 blob ,如果后端是以切片名称来做合并的,则第三个参数一定要传
controller = new AbortController() // 每一次上传切片都要新生成一个 AbortController ,否则重新上传会失败
return await axios
.post('mergeUpload', formData, { // 调用后端上传切片接口
onUploadProgress: (data) => { // 进度条展示
percentage.value = Number(
(
(Math.min(fileData.value.size, start + data.loaded) / fileData.value.size) *
100
).toFixed(2)
)
},
signal: controller.signal // 取消上传
})
.then((res) => updateUrl(res.data))
}
const cancel = () => {
dialogVisible.value = false
cancelUpload.value = true
controller?.abort()
axios.post('cancelUpload', { folder: fileData.value.md5 }) // 调用后端接口,删除已上传的切片
}
可上传本地录音录像,支持上传的
音频格式为:mp3、m4a、aac
视频格式为:mp4、m4v
上传录音录像
正在上传:{{ fileData.name }}
取消上传