React Hook+Ts+Antd+SpringBoot实现分片上传(前端)

大文件上传在C端还是很少见的, 曾经爆火的网盘应用如今也只剩寥寥几家,大家可能接触较多的就一些视频网站在上传视频的时候会碰到。在做企业内部应用经常会碰到大文件超大文件的上传、下载、存储等问题,我以前在做企业内部网盘和IOT设备接入管理的时候涉及大文件存储和分发, 实现过fastdfs分片上传, 腾讯oss分片上传和minio分片上传、秒传等功能,以前前端部份没有写过,后端是直接用的开源服务提供的api去实现的,本次我将从前端到后端(本地磁盘)重新捋一遍。

如下代码除了sparkmd5没有其他第三方库

前端分片

javascript 复制代码
文件MD5校验已经是主流方案, 咱们是在前端上传的时候就要生成MD5
npm install spark-md5 @types/spark-md5 --save-dev

//如下代码都是在一个tsx中

// 计算文件的 MD5 值
function calculateMD5(file:any) {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    const chunkSize = 5 * 1024 * 1024;
    let currentChunk = 0;
 
    fileReader.onload = function (e:any) {
      spark.append(e.target.result);
      currentChunk++;
      if (currentChunk < chunks) {
        loadNext();
      } else {
        const result = spark.end();
        resolve(result);
      }
    };
 
    // 加载下一个分片
    function loadNext() {
      const start = currentChunk * chunkSize;
      const end = Math.min(file.size, start + chunkSize);
      const buffer = file.slice ? file.slice(start, end) : file.webkitSlice(start, end); // 使用 slice 方法
      fileReader.readAsArrayBuffer(buffer);
    }
 
    const chunks = Math.ceil(file.size / chunkSize); // 文件划分成的分片数量
    loadNext(); // 开始加载第一个分片
  });
}
 
// 将文件划分成多个分片
function chunkFile(file:any, chunkSize:any) {
  const chunks = Math.ceil(file.size / chunkSize); // 文件划分成的分片数量
  const chunksList = [];
  let currentChunk = 0;
 
  while (currentChunk < chunks) {
    const start = currentChunk * chunkSize;
    const end = Math.min(file.size, start + chunkSize);
    const chunk = file.slice ? file.slice(start, end) : file.webkitSlice(start, end); // 使用 slice 方法
    chunksList.push(chunk); // 将分片添加到列表中
    currentChunk++;
  }
 
  return chunksList; // 返回分片列表
}

interface fileItem {
	id:string
	name:string
	size:string
	status:string
	type:string
	time:string
}

interface Props{
	userCode:string
	open: boolean
	close: () => void
}

function FileUpload({open, close, userCode}: Props) {

 const [uploading, setUploading] = useState(false); // 是否正在上传文件的状态
  const [progress, setProgress] = useState(0); // 文件上传进度的状态
  const chunkRefs:any = useRef([]); // 保存分片引用的引用
  const md5Ref:any = useRef(""); // 保存 MD5 值的引用
  const [fileList, setFileList] = useState([] as fileItem[])
  const fileChunkList = [] as UploadFile[] // antd官方组件组提供
 
  const handleFileChange = async ({ file }:any) => {
    setUploading(true); // 开始文件上传
    setProgress(1) //计算MD5需要时间, 文件越大时间越长, 假装给一个进度
    const md5 = await calculateMD5(file); // 计算文件的 MD5 值
    md5Ref.current = md5; // 保存 MD5 值到引用
 
    // 将文件划分成多个分片并保存到引用对象中
    const chunksList:any = chunkFile(file, 5 * 1024 * 1024);
    chunkRefs.current = chunksList.map((chunk:any, index:any) => {
      const formData = new FormData();
      // 下面的参数根据实际需要自己定义, 注意后端接口接收入参一致即可
      formData.append("file", chunk);
      formData.append("userCode", userCode);
      formData.append("fileName", file.name);
      formData.append("uuid", uuid); // 随机生成一个UUID
      formData.append("totalSize", chunksList.length);
      formData.append("chunkNumber", index.toString());
      formData.append("identifier", md5Ref.current); // 添加 MD5 参数
      return formData;
    });
	//结果
	const arr = [] as fileItem[]
	const uploadResult = {
		id: md5Ref.current || '',
		name: file.name,
		size: chunkList.length,
		status: 'uploading',
		type: '大文件上传',
		time: new Date().toISOString(),
	} as fileItem
	// 成功
	const success = () => {
		uploadResult.status = 'done'
		arr.push(uploadResult)
		setFileList([...fileList, ...arr])
		message.success('上传成功')
	}
	// 失败
	const error = () => {
		uploadResult.status = 'error'
		arr.push(uploadResult)
		setFileList([...fileList, ...arr])
		message.success('上传失败')
	}
 
 	// 合并参数
	const mergeParams = {
		identifier: md5Ref.current,
		fileName:file.name,
		totalChunks: chunksList.length,
		userCode: userCode,
		uuid: uuid
	}
    // 定义递归函数用于逐个上传分片
    const uploadChunk = async (index:any) => {
      if (index >= chunkRefs.current.length) {
        // 所有分片上传完成
        await httpPost('/marge', mergeParams).then(res => {
        	if(res && res.code === 200) {
	        	success()
	        	setUploading(false)
	        	return
        	} else {
        		error()
        		return
        	}
        })
        return;
      }
 
      try {
      	// 调用上传函数上传当前分片,此处为调用上传的接口
        await httpPost('/chunk', chunkRefs.current[index]).then(res => {
			if(res && res.code === 200) {
				console.log(`分片 ${index + 1} 上传成功`);
		        // 更新进度条的值
		        const newProgress = Math.ceil(((index + 1) / chunkRefs.current.length) * 100);
		        setProgress(newProgress);
		        // 递归调用上传下一个分片
		        await uploadChunk(index + 1);
		        return;
			}
		}); 
      } catch (error) {
        console.error(`分片 ${index + 1} 上传失败`, error);
        message.error("文件上传失败!");
        uploadResult.status = 'error'
        arr.push(uploadResult)
		setFileList([...fileList, ...arr])
        setUploading(false); // 文件上传失败,修改上传状态
        return;
      }
    };
    // 开始递归上传第一个分片
    await uploadChunk(0);
  };
	
   const handleRemove = () => {
    // 清空保存的分片引用、MD5 引用和重置进度条
    chunkRefs.current = [];
    md5Ref.current = "";
    setProgress(0);
  };

   const handleClose = () => {
   		close()
   }
	
   const getIcon = (value: string) = > {
   		// 逻辑, 根据上传状态返回不同的图标, 图标通过css自定义大小, 最好fontSize加大, 明显一些
   		// balabala
   		return '图标'
   }
 
  return (
    <Modal
    	title="Upload"
    	width="50%"
    	open={open}
    	footer={null}
    	onCancel={handleClose}
    >
      <Upload
        name="file"
        multiple={false}
        defaultFileList={fileChunkList}
        listType="pictrue"
        beforeUpload={() => false}
        onChange={handleFileChange}
        onRemove={handleRemove} // 添加自定义的删除操作
      >
        <Button loading={uploading} icon={<UploadOutlined />}>
          {uploading ? progress === 100 ? "合并中" : "上传中" : "选择文件"}
        </Button>
      </Upload>
      // 显示文件上传进度条
      {uploading && <Progress percent={progress} status="active" />}
      // 我通过css隐藏了uplaod组件自带的文件列表, 自定义一个自己的list列表展示
      {
      	fileList && <List
      		hearder={<div>上传列表</div>}
      		itemLayout="horizontal"
      		dataSource={fileList}
      		randerItem={(item, index) => (
      		<List.Item>
      			<List.Item.Mate
	      			key={index}
	      			avatar={getIcon(item.status)}
	      			title={item.name}
	      			description={`文件大小:${item.size} 文件上传状态: ${item.status}....自定义即可`}
      		     
      	          />
      	    </List.Item>
      	  )}
      	/>
      }
    </Modal>
  );
}

以上就是前端的核心逻辑部份,有两个filelist是我本地还有两个上传按钮都会往这里push文件一起展示。

前端在获取文件MD5的时候随着文件大小时间成正比,所以也要考虑是否可以单独抽出一个初始化接口过渡一下,或者使用虚假的进度进行隐藏。

根据实际业务,参数自定义,还有一些拓展功能会集成到代码里面,例如上传图片压缩缩略图,上传视频按帧取图做封面活着缩略图,文档转换,大文件MD5检验算法优化等等,还有一个问题就是大文件存储问题,全局看成本最重要,可以结合云存储去交互一些热文件,而且有时候带宽的限制也不得不这么做。

下次我将更新SpringBoot部份的代码,其实网上有很多,大差不差,都是接收分片按序号命名然后合并,可以看看其他人的作品。建议,如果不是其他业务需要直接加载文件进行二次处理,文件的管理还是使用成熟的文件服务器策略比较好。

相关推荐
汇能感知7 小时前
光谱相机的未来趋势
经验分享·笔记·科技
老顾聊技术9 小时前
目标检测在工厂制造中的创新应用与实践
经验分享
go_bai10 小时前
Linux--常见工具
linux·开发语言·经验分享·笔记·vim·学习方法
jingwang-cs14 小时前
国检集团官网UI设计展示——专业界面设计实力呈现
经验分享
我要学习别拦我~14 小时前
逻辑回归中的决策边界解析与应用实例
经验分享·机器学习·逻辑回归
老师可可17 小时前
Excel学生成绩表,如何生成成绩分析报告?
经验分享·学习·小程序·excel·学习方法
艾莉丝努力练剑19 小时前
【C++STL :string类 (二) 】从接口应用到内存模型的全面探索
linux·开发语言·c++·经验分享
熊猫不是猫QAQ19 小时前
老字号传承,达尔优AE6电竞鼠标!熟悉的味道,时代的配方
经验分享
空影学Java21 小时前
Day71 基本情报技术者 单词表06 计算理论与性能
经验分享
源代码•宸1 天前
GAMES101:现代计算机图形学入门(Chapter2 向量与线性代数)迅猛式学线性代数学习笔记
经验分享·笔记·学习·线性代数·计算机图形学