欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 艺术 > 文件分片上传demo(ant design vue 的a-upload)

文件分片上传demo(ant design vue 的a-upload)

2025/4/2 14:46:30 来源:https://blog.csdn.net/m0_58537749/article/details/146566925  浏览:    关键词:文件分片上传demo(ant design vue 的a-upload)

1.问题:

使用a-upload组件上传文件时,超过大小200m的文件无法上传

2.解决办法:

文件分片上传

3.代码部分

//spark-md5 是一个用于计算 MD5 值的库,支持对字符串和数组缓冲区(ArrayBuffer)进行 MD5 计算。
import SparkMD5 from 'spark-md5' 
const DEFAULT_SIZE = 20 * 1024 * 1024 //设置每次读取文件时的块大小。20 * 1024 * 1024 表示 20MB
// fileMd5用于计算文件的 MD5 值。它接受两个参数:
//   file:要计算 MD5 值的文件对象。
//   chunkSize:每个分块的大小,默认值为 DEFAULT_SIZE(20MB)。
const fileMd5 = (file, chunkSize = DEFAULT_SIZE) => {return new Promise((resolve, reject) => {const startMs = new Date().getTime(); //记录开始计算 MD5 的时间//blobSlice获取文件对象的 slice 方法。不同浏览器可能有不同的实现方式,这里做了兼容性处理,确保在所有浏览器中都能正确分块文件。let blobSlice =												File.prototype.slice ||File.prototype.mozSlice ||File.prototype.webkitSlice;//计算文件总共需要分成多少块let chunks = Math.ceil(file.size / chunkSize);//初始化当前正在处理的块编号,从 0 开始let currentChunk = 0;let spark = new SparkMD5.ArrayBuffer(); //追加数组缓冲区。let fileReader = new FileReader(); //读取文件//onload函数在文件块读取完成时触发fileReader.onload = function (e) {//将当前块的数组缓冲区内容追加到 SparkMD5 实例中,用于计算 MD5 值。spark.append(e.target.result);currentChunk++;if (currentChunk < chunks) { //文件块未处理完成继续函数loadNext();} else {const md5 = spark.end(); //完成md5的计算,返回十六进制结果。console.log('文件md5计算结束,总耗时:', (new Date().getTime() - startMs) / 1000, 's')//调用 resolve 将计算结果返回给调用者resolve(md5);}};//当读取文件块时发生错误时,调用 reject 将错误传递给调用者fileReader.onerror = function (e) {reject(e);};//继续处理函数function loadNext() {console.log('当前part number:', currentChunk, '总块数:', chunks);let start = currentChunk * chunkSize;let end = start + chunkSize;(end > file.size) && (end = file.size);fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();});
}
// 解释:定义一个函数 md5,用于计算字符串的 MD5 值。它接受两个参数:
// fileName:文件名。
// fileSize:文件大小。
const md5 = (fileName, fileSize) => {let md5Hash = '';const combinedString = fileName + '' + fileSize;// 使用 SparkMD5 计算字符串的 MD5 值const spark = new SparkMD5();spark.append(combinedString);md5Hash = spark.end();return md5Hash;
}export default md5

这段代码提供了两种计算 MD5 值的方法:

  1. 文件的 MD5 值计算:通过分块读取文件并逐块计算 MD5,适用于大文件,避免一次性加载整个文件到内存中。
  2. 字符串的 MD5 值计算:直接对文件名和文件大小的组合字符串进行 MD5 计算,适用于快速生成文件的唯一标识。
import { defHttp } from '@/utils/http/axios';const basicApi = '/file/minio/chuncked';
//定义一个枚举 Api,包含所有文件分片上传相关的接口路径。
// TASK_INFO:初始化任务或获取任务信息的接口。
// SELECT_TASK:获取任务列表的接口。
// INIT_TASK:初始化分片上传任务的接口。
// MERGE:合并分片的接口。
enum Api {TASK_INFO = basicApi + '/init',SELECT_TASK = basicApi + '/minioList',INIT_TASK = basicApi,MERGE = basicApi + '/merge',
}export interface UploadPageIM {identifier: string;fileName: string;totalSize: any;chunkSize: number;isPublic: string;groupId: string;
}export interface PreSignUrlIM {identifier: string;partNumber: string;
}/*** 根据文件的md5获取未上传完的任务* @param identifier 文件md5* @returns {Promise<AxiosResponse<any>>}*/
export const taskInfo = (identifier) =>defHttp.get<any>({ url: Api.INIT_TASK + '/' + identifier });/*** 获取所有的url地址信息用于展示*/
export const selectTask = (groupId) =>defHttp.get<any>({ url: Api.SELECT_TASK + '/' + groupId });/*** 初始化一个分片上传任务* @param identifier 文件md5* @param fileName 文件名称* @param totalSize 文件大小* @param chunkSize 分块大小* @returns {Promise<AxiosResponse<any>>}*/
export const initTask = (params: UploadPageIM) =>defHttp.post({ url: Api.TASK_INFO, params });/*** 获取预签名分片上传地址* @param fileId 文件md5* @param partNumber 分片编号* @returns {Promise<AxiosResponse<any>>}*/
export const preSignUrl = (fileId, partNumber) =>defHttp.get<any>({ url: Api.INIT_TASK + '/' + fileId + '/' + partNumber });/*** 合并分片* @param fileId* @returns {Promise<AxiosResponse<any>>}*/
export const merge = (fileId) =>defHttp.post({ url: Api.MERGE + '/' + fileId });逐行解释
<template><div class="upload-page-style w-full h-full"><a-upload //ant design vue 上传组件v-model:file-list="fileList" //绑定文件列表,用于显示上传的文件状态name="file" //上传文件的字段名。:headers="headers" //上传时携带的 HTTP 头。:progress="progress"//自定义上传进度条的样式@change="handleChange" :custom-request="handleHttpRequest" //自定义上传事件:on-remove="handleRemoveFile" //文件被移除时的回调函数drag><upload-outlined></upload-outlined><span class="pl-4 cursor-pointer">请点击此处上传</span></a-upload></div>
</template>
<script lang="ts" setup>
import axios from 'axios';
import { Card, message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import { ref, onMounted } from 'vue';
import Queue from 'promise-queue-plus';
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
import md5 from '../../api/uploadpage/md5';
import { taskInfo, initTask, preSignUrl, merge, selectTask } from '../../api/uploadpage/uploadpage.api';
import { buildShortUUID } from '@/utils/uuid';const fileUploadChunkQueue = ref({}).value //存储每个文件的上传队列
const fileList = ref([]);
const minioList = ref([]);
const progress: UploadProps['progress'] = {strokeColor: {'0%': '#108ee9','100%': '#87d068',},strokeWidth: 3,format: percent => `${parseFloat(percent)}%`,class: 'test',
};
const headers = { authorization: 'authorization-text' };// 更新文件列表中的上传进度
const updateFileListProgress = (uid: string, percent: number) => {const file = fileList.value.find((item) => item.uid === uid);if (file) {file.percent = percent;if(file.percent == 100) {file.status ='done';}}
};/*** 自定义上传方法入口*/const handleHttpRequest = async (options) => {const file = options.file; //当前上传的文件对象try {// 获取或初始化上传任务const task = await getTaskInfo(file); //调用 getTaskInfo 函数,获取或初始化上传任务console.log('task',task);if (!task) {throw new Error('任务初始化失败');}const { finished, taskRecord, path } = task; //解构// 如果任务已完成,直接返回成功if (finished) {options.onSuccess(path, file); //调用 options.onSuccess 回调函数return;}// // 执行分片上传await handleUpload(file, taskRecord, options);// // 合并分片await merge(taskRecord.fileId).then(() => {message.success(`${file.name} 文件上传成功`);options.onSuccess('上传成功', file);file.status = 'done';}).catch(() => {message.error(`${file.name} 文件上传失败`);options.onError(new Error('文件上传失败'));});} catch (error) {message.error(`${file.name} 上传失败: ${error.message}`);options.onError(error);}};/*** 获取一个上传任务,没有则初始化一个*/const getTaskInfo = async (file) => {const identifier = await md5(file.name, file.size); //计算文件的 MD5 值//const identifier = (file.name + '' + file.size).substring(1,32);const msg = await taskInfo(identifier);if(msg) {return msg; }// 初始化新任务const initTaskData = {md5: identifier,fileName: file.name,totalSize: file.size,chunkSize: 10 * 1024 * 1024, // 每个分片大小isPublic: '0',// groupId: buildShortUUID(),groupId: '_66771548521732506784121',};try{const initResult = await initTask(initTaskData);console.log('initResult',initResult);return initResult;}catch(error) {message.error('文件上传错误');}throw new Error('文件上传错误');};/*** 上传逻辑处理,如果文件已经上传完成(完成分块合并操作),则不会进入到此方法中*/const handleUpload = async (file, taskRecord, options) => {const { chunkSize, chunkNum, fileId, exitPartList = [] } = taskRecord;let lastUploadedSize = 0; // 上次断点续传时上传的总大小let uploadedSize = 0; // 已上传的大小const totalSize = file.size || 0; // 文件总大小const startMs = new Date().getTime(); // 上传开始时间// 获取从开始上传到现在的平均速度(byte/s)const getSpeed = () => {// 已上传的总大小 - 上次上传的总大小(断点续传)= 本次上传的总大小(byte)const intervalSize = uploadedSize - lastUploadedSize;const nowMs = new Date().getTime();// 时间间隔(s)const intervalTime = (nowMs - startMs) / 1000;return intervalSize / intervalTime; // 返回速度 (byte/s)};const uploadNext = async (partNumber) => {//TODO 不知道为什么chunkSize类型是字符型const start = chunkSize * (partNumber - 1);const end = (start - 0) + (chunkSize - 0);const blob = file.slice(start, end); //获取当前分片的内容console.log('start:'+start+' end:'+end)const signalData = await preSignUrl(fileId,partNumber);//调用 preSignUrl 函数,获取预签名的上传地址//获取预签名的上传地址let signalUrl = signalData?.url;//如果获取到了上传地址if (signalUrl) {await axios.request({url: signalUrl,method: 'PUT',data: blob,headers: {'Content-Type': 'application/octet-stream'}})return Promise.resolve({ partNumber: partNumber, uploadedSize: blob.size })} else {return Promise.reject(`分片${partNumber}, 获取上传地址失败`)}};/*** 更新上传进度* @param increment 为已上传的进度增加的字节量,increment:当前分片的大小*/const updateProcess = (increment) => {const { onProgress } = optionslet factor = 1000; // 每次增加1000 bytelet from = 0;// 通过循环一点一点的增加进度while (from <= increment) {from += factoruploadedSize += factorconst percent = Math.round(uploadedSize / totalSize * 100).toFixed(2);onProgress({percent: percent})updateFileListProgress(file.uid, percent)}const speed = getSpeed();const remainingTime = speed != 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知'console.log('全部大小', totalSize)console.log('已上传大小', uploadedSize)console.log('剩余大小:', (totalSize - uploadedSize) / 1024 / 1024, 'mb');console.log('当前速度:', (speed / 1024 / 1024).toFixed(2), 'mbps');console.log('预计完成:', remainingTime);}return new Promise(resolve => {const failArr = []; //存储失败的分片const queue = Queue(5, { //初始化一个上传队列,最大并发数为 5"retry": 3,               //每个分片的最大重试次数"retryIsJump": false,     //是否立即重试"workReject": function(reason,queue){ //处理失败的分片failArr.push(reason) },"queueEnd": function(queue){ //队列结束时的回调函数resolve(failArr); }})//将队列存储到 fileUploadChunkQueuefileUploadChunkQueue[file.uid] = queuefor (let partNumber = 1; partNumber <= chunkNum; partNumber++) {const exitPart = (exitPartList || []).find(exitPart => exitPart.partNumber == partNumber)if (exitPart) {// 分片已上传完成,累计到上传完成的总额中,同时记录一下上次断点上传的大小,用于计算上传速度lastUploadedSize += exitPart.size updateProcess(exitPart.size)} else {queue.push(() => uploadNext(partNumber).then(res => {// 单片文件上传完成再更新上传进度updateProcess(res.uploadedSize)}))}}if (queue.getLength() == 0) {// 所有分片都上传完,但未合并,直接return出去,进行合并操作resolve(failArr);return;}queue.start()})};/*** 移除文件列表中的文件* 如果文件存在上传队列任务对象,则停止该队列的任务*/const handleRemoveFile = (file) => {// 如果有对应上传队列,停止其上传if (fileUploadChunkQueue[file.uid]) {fileUploadChunkQueue[file.uid].stop();delete fileUploadChunkQueue[file.uid];}message.info(`已移除文件 ${file.name}`);};</script>

这段代码实现了一个完整的文件分片上传功能,包括:

  1. 文件列表管理:通过 fileList 管理上传的文件列表。
  2. 自定义上传逻辑:通过 handleHttpRequest 自定义上传逻辑。
  3. 任务初始化:通过 getTaskInfo 获取或初始化上传任务。
  4. 分片上传:通过 handleUpload 处理分片上传。
  5. 进度更新:通过 updateFileListProgress 更新文件列表中的上传进度。
  6. 文件移除:通过 handleRemoveFile 移除文件列表中的文件。

总结:

涵盖了文件分片的基本做法,具体接口要看前后端商议,这里的后端接口的要求基本也在注释标注,个人理解,文件分片是切割+合并,md5方法可以拿过去直接用,文件唯一性标识也可以根据项目而定。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词