1.分片上传hook
需要后端提供三个接口,初始化接口、上传分片接口、合并分片接口。
可以先将uploadInOrder函数(上传每个分片)折叠,看完其他逻辑再看uploadInOrder内容。
这个hook实现了 切割分片并上传、自定义分片大小、随时终止上传、回显上传进度 等功能
// 上传大文件,分片处理
import {
initiateMultipartUpload,
uploadPart,
completeMultipartUpload,
} from '@/service/index/upload'
/**
* 分片上传
* @param formData 上传文件信息tempFilePath: 文件路径, fileSize: 文件大小, uploadOssUrl: 上传到云端的路径
* @param callBack 上传回调success: 是否成功, result: 上传结果, err: 错误信息
* @param options 上传配置chunkSize: 分片大小, onProgress: 上传进度, shouldAbort: 是否中止上传
*/
export default function useMultipart<T = string>(
formData: {
tempFilePath: string
fileSize: number
uploadOssUrl: string
},
callBack: (success: boolean, result?: T, err?: any) => void,
options?: {
chunkSize?: number
onProgress?: (progress: number) => void
shouldAbort?: () => boolean
},
) {
const defaultOptions = {
chunkSize: formData.fileSize > 50 * 1024 * 1024 ? 5 * 1024 * 1024 : 2 * 1024 * 1024, // 默认分块大小:大于50MB时5MB每块,否则2MB每块
...options,
}
// 分块上传流程
const uploadProcess = async () => {
try {
// 1. 读取文件的buffer格式
const file = uni.getFileSystemManager().readFileSync(formData.tempFilePath)
// 2. 初始化分片上传
const initResponse = await initiateMultipartUpload({
object_name: formData.uploadOssUrl,
})
const uploadId = initResponse.data.upload_id // 获取上传ID
// 3. 分块处理
const chunkCount = Math.ceil(formData.fileSize / defaultOptions.chunkSize)
const chunks = Array.from({ length: chunkCount }, (_, i) => i)
const uploadedParts: { PartNumber: number; ETag: string }[] = []
// 单片上传
const uploadInOrder = async (chunkNumber: number) => {
if (defaultOptions.shouldAbort?.()) {
throw new Error('Upload aborted by user')
}
const start = chunkNumber * defaultOptions.chunkSize
const end = Math.min(start + defaultOptions.chunkSize, formData.fileSize)
const chunkData = (file as ArrayBuffer).slice(start, end)
console.log('chunkNumber', chunkNumber, new Date().getTime())
const partResponse = await uploadPart({
upload_id: uploadId,
object_name: formData.uploadOssUrl,
content: uni.arrayBufferToBase64(chunkData),
part_number: chunkNumber + 1,
})
// ETag是接口返回值,是最后合并时需要的标识,方便合并时查到具体哪个分片
uploadedParts.push({
PartNumber: chunkNumber + 1,
ETag: partResponse.data.ETag,
})
// 更新上传进度
const progress = Math.round(((chunkNumber + 1) / chunkCount) * 100)
defaultOptions.onProgress?.(progress)
}
// 所有分片按顺序执行上传的函数
const uploadItems = async (items: number[]) => {
for (const item of items) {
// 用户手动终止上传
if (defaultOptions.shouldAbort?.()) {
throw new Error('Upload aborted by user')
}
await uploadInOrder(item)
}
}
// 4. 开始上传所有分片
await uploadItems(chunks)
// 5. 完成上传前检查用户是否选择中止
if (defaultOptions.shouldAbort?.()) {
throw new Error('Upload aborted by user')
}
// 6. 执行合并操作
const completeResponse = await completeMultipartUpload({
upload_id: uploadId, // 初始化上传时获取的id
object_name: formData.uploadOssUrl, // 需要上传到的云端路径
upload_parts: uploadedParts, // 上传的所有分片合集
})
callBack(true, completeResponse.data as T)
} catch (err) {
callBack(false, undefined, err?.message === 'Upload aborted by user' ? 'aborted' : err)
}
}
uploadProcess()
}
2.使用分片上传hook示例
各种前置变量创建
// 生成uuid的函数
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
// 路径示例,实际以uni.chooseMedia获取到的tempFilePath为准,建议大小和格式限制也一并校验
const filePath = 'http://tmp/g8eLrsqW4dhSa50a1fa970c086b21f6c06985413e0d6.mp4'
// 文件后缀
const videoFormat = filePath.split('.').pop()
// 用户id和随机uuid生成一个云端文件路径(即将要上传到云端的路径)
const uploadOssUrl = userId + '/video/' + generateUUID() + '.' + videoFormat
// 显示进度条弹框
showUploadInfo.value = true
主要调用方法
// 调用分片上传hook
useMultipart(
{
tempFilePath: firstFile.tempFilePath, // 临时文件路径
fileSize: firstFile.size, // 文件大小
uploadOssUrl, // 上传地址
},
(success, result, err) => {
console.log('上传结果', success, result, err)
if (success) {
toast.success('上传成功!')
} else {
toast.show('上传失败!')
}
// 关闭进度条弹框,并将进度归零
showUploadInfo.value = false
uploadProgress.value = 0
},
{
onProgress: (res) => {
console.log('progress', res)
// 随时更新进度百分比
uploadProgress.value = res
},
// 传入showUploadInfo的值传给hook,当用户点击取消时,showUploadInfo弹框关闭,hook就会停止上传
shouldAbort: () => {
return !showUploadInfo.value
},
},
)