提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 一、大文件上传的核心问题
- 二、解决方案:分片上传
- 2.1 核心定义
- 2.2 核心优势
- 三、分片上传核心原理
- 3.1 整体流程
- 3.2 关键:文件读取与分片
- 3.2.1 读取本地文件
- 3.2.2 文件分片实现
- 3.2.3 分片特点
- 四、核心:文件唯一标识(Hash计算)
- 4.1 为什么需要Hash值?
- 4.2 Hash计算方案
- 4.2.1 安装依赖
- 4.2.2 实现Hash计算
- 4.2.3上传分片
- 4.3 秒传功能原理
- 五、断点续传核心逻辑
- 六、总结
一、大文件上传的核心问题
当上传视频等大文件时,直接单次上传会面临诸多问题,这也是B站等平台采用分片上传的核心原因:
上传耗时极长:大文件数据量庞大,单次请求传输链路长,等待时间久;
失败代价高:网络中断、服务器异常等问题会导致上传失败,需重新完整上传;
服务端限制:多数服务器会对单次上传文件大小设限,直接上传易触发限制;
用户体验差:无进度反馈、失败后重复操作,严重影响使用感受。
二、解决方案:分片上传
2.1 核心定义
将大文件切割为多个大小均等的数据小块(分片),逐个或并行上传至服务器,全部分片上传完成后,由后端按顺序组装还原为完整文件的技术方案。
2.2 核心优势
降低失败风险:仅需重新上传出错的分片,无需重复传输完整文件;
提升上传效率:支持多分片并行上传,突破单文件传输瓶颈;
适配服务端限制:单个分片大小可控,避免触发服务端文件大小限制;
支持断点续传:结合状态记录,可在网络恢复后继续上传未完成部分。
三、分片上传核心原理
3.1 整体流程
前端读取本地文件,获取File对象;
按固定大小切割File对象,生成多个分片(Blob类型);
计算文件唯一标识(hash值),用于服务端识别文件;
逐个/并行上传分片,携带文件标识、分片序号等信息;
服务端接收分片并存储,记录分片顺序与位置;
所有分片上传完成后,前端通知后端触发文件组装;
后端按分片顺序拼接,还原为完整文件。
3.2 关键:文件读取与分片
3.2.1 读取本地文件
通过监听input标签的change事件,在回调中获取用户选择的File对象(FileList伪数组形式,需通过下标获取单个文件)。
<template><!--文件选择输入框--><input @change="handleUpload"type="file"/></div></template><script setup lang="ts">/** * 处理文件选择事件 * @param e 事件对象 */consthandleUpload=(e:Event)=>{// 类型断言:将事件目标转为HTMLInputElementconstinput=e.targetasHTMLInputElement;// 获取文件列表(伪数组,支持下标访问)constfiles=input.files;if(!files||files.length===0)return;// 获取第一个选中的文件(File对象,继承自Blob)constfile=files[0];console.log("选中文件信息:",{name:file.name,// 文件名size:file.size,// 文件大小(字节)type:file.type// 文件MIME类型});};</script>3.2.2 文件分片实现
核心依赖Blob.slice()方法:File对象继承自Blob,可直接调用该方法切割文件。通过循环控制切割的起始与结束位置,生成多个固定大小的分片。
关键参数:CHUNK_SIZE(分片大小,通常设为1-5MB,此处以1MB为例)
// 分片大小:1MB(1024*1024字节)constCHUNK_SIZE=1024*1024;/** * 切割文件为多个分片 * @param file 待分片的File对象 * @returns 分片数组(每一项为Blob类型) */constcreateChunks=(file:File):Blob[]=>{constchunks:Blob[]=[];letcurrentPosition=0;// 当前切割位置// 循环切割,直到覆盖整个文件while(currentPosition<file.size){/** * slice(start, end):切割Blob * start:起始字节位置 * end:结束字节位置(不包含) */constchunk=file.slice(currentPosition,currentPosition+CHUNK_SIZE);chunks.push(chunk);// 更新下一次切割的起始位置currentPosition+=CHUNK_SIZE;}console.log(`文件分片完成,共${chunks.length}个分片`);returnchunks;};// 调用示例(在handleUpload中添加)// const file = files[0];// const chunks = createChunks(file);3.2.3 分片特点
分片过程几乎瞬间完成,原因是:Blob/File对象仅存储文件元信息(名称、大小等),不存储实际文件数据,slice()方法仅生成新的元信息引用,未复制数据。
四、核心:文件唯一标识(Hash计算)
4.1 为什么需要Hash值?
服务端需通过唯一标识区分不同文件,避免以下问题:
文件名重复:不同文件可能重名,相同文件可能改名;
实现秒传:相同内容的文件仅需上传一次;
断点续传:通过Hash值匹配已上传的分片。
4.2 Hash计算方案
采用spark-md5库(轻量、高效,支持浏览器端),基于文件内容生成唯一MD5 Hash值。为优化大文件计算效率,采用「抽样计算」策略:
第一个和最后一个分片:完整参与计算;
中间分片:仅抽取头部2字节、中间2字节、尾部2字节参与计算;
兼顾唯一性与性能,避免全量计算耗时过长。
4.2.1 安装依赖
# npm安装npminstallspark-md5 --save# yarn安装yarnaddspark-md5# pnpm安装pnpmaddspark-md54.2.2 实现Hash计算
importSparkMD5from"spark-md5";/** * 计算文件的唯一Hash值(基于内容) * @param chunks 文件分片数组 * @returns 文件Hash值(Promise) */constcalculateFileHash=(chunks:Blob[]):Promise<string>=>{returnnewPromise((resolve,reject)=>{constspark=newSparkMD5.ArrayBuffer();// 基于ArrayBuffer计算constfileReader=newFileReader();// 用于读取分片数据constsampleChunks:Blob[]=[];// 用于抽样的分片数据// 1. 抽样策略:选取部分数据参与计算chunks.forEach((chunk,index)=>{constisFirstChunk=index===0;constisLastChunk=index===chunks.length-1;if(isFirstChunk||isLastChunk){// 首/尾分片:完整加入抽样sampleChunks.push(chunk);}else{// 中间分片:抽取头部2B、中间2B、尾部2BsampleChunks.push(chunk.slice(0,2));sampleChunks.push(chunk.slice(CHUNK_SIZE/2,CHUNK_SIZE/2+2));sampleChunks.push(chunk.slice(CHUNK_SIZE-2,CHUNK_SIZE));}});// 2. 读取抽样数据fileReader.readAsArrayBuffer(newBlob(sampleChunks));// 3. 读取完成后计算HashfileReader.onload=(e)=>{try{constarrayBuffer=e.target?.resultasArrayBuffer;spark.append(arrayBuffer);// 追加数据constfileHash=spark.end();// 生成最终Hash值console.log("文件唯一Hash值:",fileHash);resolve(fileHash);}catch(error){reject("Hash计算失败:"+error);}};// 4. 读取失败处理fileReader.onerror=(error)=>{reject("文件读取失败:"+error);};});};// 调用示例(在handleUpload中添加)// const chunks = createChunks(file);// const fileHash = await calculateFileHash(chunks);4.2.3上传分片
constuploadChunks=async(chunks:Blob[])=>{constdata=chunks.map((chunk,index)=>{return{fileHash:fileHash.value,chunkHash:fileHash.value+"-"+index,chunk,};});constformDatas=data.map((item)=>{constformData=newFormData();formData.append("fileHash",item.fileHash);formData.append("chunkHash",item.chunkHash);formData.append("chunk",item.chunk);returnformData;});// console.log(formDatas)constmax=6;letindex=0;consttaskPool:any=[];//请求池while(index<formDatas.length){consttask=fetch("/upload",{method:"POST",body:formDatas[index],});taskPool.splice(taskPool.findIndex((item:any)=>item===task))taskPool.push(task)if(taskPool.length===max){awaitPromise.race(taskPool);//taskPool里如果有一个已完成,那么这个Promise状态就会标志城已完成}index++;}awaitPromise.all(taskPool);//方志友请求未完成};4.3 秒传功能原理
用户上传文件前,前端先计算文件Hash值并传给服务端;
服务端查询该Hash值是否已存在(对应已上传的完整文件);
若存在:直接返回“上传成功”,无需传输文件(秒传效果);
若不存在:正常执行分片上传流程。
五、断点续传核心逻辑
基于文件Hash值实现,核心是“上传前校验已传分片”:
前端计算文件Hash后,向服务端发起“查询已传分片”请求;
服务端返回该文件已上传的分片序号列表(如:[0,1,2,5]);
前端过滤掉已上传的分片,仅上传未完成的分片;
所有分片上传完成后,请求服务端组装文件。
六、总结
前端核心职责:文件读取 → 分片切割 → Hash计算 → 分片上传 → 触发组装
后端核心职责:接收分片 → 存储分片 → 校验已传分片 → 组装完整文件
关键技术:Blob.slice()分片、spark-md5 Hash计算、FormData传输分片