自己实现一个大文件切片上传+断点续传
PM:喂,那个切图仔,我这里有个100G的视频要上传,你帮我做一个上传后台,下班前给我哦,辛苦了。
我:。。。
相信每个切图工程师,都接触过文件上传的application需求,一般的小文件,我们直接使用 input file
,然后构造一个 new FormData()
对象,扔给后端就可以了。如果使用了 Ant d线程和进程的区别是什么esign 或者 element ui 之类的ui库,那更简单,直接调用一下api即可。当然了,复杂一些的,ios越狱市面上也有不少优秀的第三方ios是苹果还是安卓插件,比如WebUploader。但是作为一个javascript:void(0)有追求的工程师,怎么能仅仅满足于使用插件呢,今天我们就来ios应用商店自己实现一个。
首先我们来分析一下需求
一个上传组件,需要具备编辑器135的功能:
- 需要校验文件格式
- 可以上传任何文件,包括超大的视频文件(切片)
- 上传期间JavaScript断网后,再次联ios14.4.1更新了什么网可以继续上传(断点续传)
- 要approach有进度条提示
- 已经上传过同一个文件后,直接上传完成(秒传)
前后端分工:
- 前端:
- 文件格式校验
- 文件切片、md5APP计算
- 发application起检javascript高级程序设计查请求javascript百炼成仙免费阅读,把当前文件的hashapproach发送给服务端,检查是否有相同hash的文件
- 上传进度计算
- 上传完成后通知后端合并切片
- 后端:
- 检查接收到的hash是否有相同的文件,并通知前端当前hash是否有未完成的上传
- 接收切片
- 合并所有切片
架构图如下
接下来开始具体实现
一、 格式校验
对于上传的文件,一般来说,我们要校验其格式,仅需要获取文件的后缀(扩展名),即可判断其是否符合javascript:void(0)我们的上传限制:
//文件路径
varfilePath="file://upload/test.png";
//获取最后一个.的位置
varindex=filePath.lastIndexOf(".");
//获取后缀
varext=filePath.substr(index+1);
//输出结果
console.log(ext);
//输出:png
但是,这种方式有个弊端,那就是我们可以随便篡改文件的后缀名,比如:test.mp4
,我们可以通过修改其后缀名:test.mp4 -> test.png
,这样ios越狱即可绕过限制进行上传。那有没编辑器手机版有更严格的限制方式呢?当然是有的。
那就是通过查看文件的二进制数据来识线程数是什么别其真实的文件类型,因为计算机识别文件类型时,并不是真的通过文件的后缀名来识别的,而是通过 “魔数”(Magic Number)来区分,对于某一approve些类型的文件,起始的几个字节内容都是固定的,根据这几个字节的内容就可以判断文件的类型。借助javascript面试题十六进制编辑器,可以查看一下图片的二进制数据,我们还是以test.png
为例:
由上图可知,PNG 类型的图片前 8 个字节是 0x89 50 4E 47 0D 0A 1A 0A
。基于javascript:void(0)这个结编辑器小说果,编辑器小说我们可以据线程池此来做文件编辑器英语的格式校验,以vue项目为例:
<template>
<div>
<input
type="file"
id="inputFile"
@change="handleChange"
/>
</div>
</template>
<script>
exportdefault{
name:"HelloWorld",
methods:{
check(headers){
return(buffers,options={offset:0})=>
headers.every(
(header,index)=>header===buffers[options.offset+index]
);
},
asynchandleChange(event){
constfile=event.target.files[0];
//以PNG为例,只需要获取前8个字节,即可识别其类型
constbuffers=awaitthis.readBuffer(file,0,8);
constuint8Array=newUint8Array(buffers);
constisPNG=this.check([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]);
//上传test.png后,打印结果为true
console.log(isPNG(uint8Array))
},
readBuffer(file,start=0,end=2){
//获取文件的二进制数据,因为我们只需要校验前几个字节即可,所以并不需要获取整个文件的数据
returnnewPromise((resolve,reject)=>{
constreader=newFileReader();
reader.onload=()=>{
resolve(reader.result);
};
reader.onerror=reject;
reader.readAsArrayBuffer(file.slice(start,end));
});
}
}
};
</script>
以上为校验文件类型的方法,对于其他类型的文件,比如mp4,xsl等,大家感兴趣的话javascript权威指南,也可以通过工具查线程撕裂者看其二进制数据,以此来做格式校验appear。
以下为编辑器手机版汇总的一些文件的二进制标识:javascript:void(0)
1.JPEG/JPG-文件头标识(2bytes):ff,d8文件结束标识(2bytes):ff,d9
2.TGA-未压缩的前5字节0000020000-RLE压缩的前5字节0000100000
3.PNG-文件头标识(8bytes)89504E470D0A1A0A
4.GIF-文件头标识(6bytes)4749463839(37)61
5.BMP-文件头标识(2bytes)424DBM
6.PCX-文件头标识(1bytes)0A
7.TIFF-文件头标识(2bytes)4D4D或4949
8.ICO-文件头标识(8bytes)0000010001002020
9.CUR-文件头标识(8bytes)0000020001002020
10.IFF-文件头标识(4bytes)464F524D
11.ANI-文件头标识(4bytes)52494646
二、 文件切片
假设我们要把一个1G的视频,分割为每块1MBappetite的切片,可定义 DefualtChunkSize = 1 * 1024 * 1024
,通过 spark-md5
来计算文件内容的hash值。那如何分割文件呢,使用文件对象File的方法File.prototype.slice
即可ios模拟器。
需要注意的是,切割一个较大的文件,比如10G,那分割为1Mb大小的话,将会生成一万个切片,众所周知,js是单线appetite程模型,如果这个计算过程在主线程中的话,那我们的页面必然会直approve接崩溃,这时,就该我们的 Web Worker
来上场了。
Web W线程池面试题orker 的作用,就是为 Javajavascript是干什么的Script 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。具体的作用,不了解的同学可以自行去学些一下。这里就不展开讲了。
以下为部分关键代码:
//upload.js
//创建一个worker对象
constworker=newworker('worker.js')
//向子线程发送消息,并传入文件对象和切片大小,开始计算分割切片
worker.postMessage(file,DefualtChunkSize)
//子线程计算完成后,会将切片返回主线程
worker.onmessage=(chunks)=>{
...
}
子线程代码:
//worker.js
//接收文件对象及切片大小
onmessage(file,DefualtChunkSize)=>{
letblobSlice=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,
chunks=Math.ceil(file.size/DefualtChunkSize),
currentChunk=0,
spark=newSparkMD5.ArrayBuffer(),
fileReader=newFileReader();
fileReader.onload=function(e){
console.log('readchunknr',currentChunk+1,'of');
constchunk=e.target.result;
spark.append(chunk);
currentChunk++;
if(currentChunk<chunks){
loadNext();
}else{
letfileHash=spark.end();
console.info('finishedcomputedhash',fileHash);
//此处为重点,计算完成后,仍然通过postMessage通知主线程
postMessage({fileHash,fileReader})
}
};
fileReader.onerror=function(){
console.warn('oops,somethingwentwrong.');
};
functionloadNext(){
letstart=currentChunk*DefualtChunkSize,
end=((start+DefualtChunkSize)>=file.size)?file.size:start+DefualtChunkSize;
letchunk=blobSlice.call(file,start,end);
fileReader.readAsArrayBuffer(chunk);
}
loadNext();
}
以上利用worker线程,我们即可得到计算后的切片,以及md5值。
三、javascript面试题 断点续传 + 秒传编辑器 + 上传进度
在拿到切片和md5后,我们首编辑器先去服ios系统务器查询一下,javascriptdownload是否已经存在当前文件。
- 如果已存在,并且已经是上传成功的文件,则直线程安全接返回前端上传成功,即可实现”秒传”。
- 如果已存在,并且有一部分切片上传失败,则返回给前端已经上传成功的切片njavascript面试题ame,前端拿到后,根据返回的切片,计算出未上传成功的剩余切片,然后把剩余的切片继续上传,线程池面试题即可实现”断点续传”。
- 如果不存在,javascriptdownload则开始上传,这里需要注意的是,在并发上传切片时,需要控制并发量,避免一次性上传过多切片,导致崩溃。
//检查是否已存在相同文件
asyncfunctioncheckAndUploadChunk(chunkList,fileMd5Value){
constrequestList=[]
//如果不存在,则上传
for(leti=0;i<chunkList;i++){
requestList.push(upload({chunkList[i],fileMd5Value,i}))
}
//并发上传
if(requestList?.length){
awaitPromise.all(requestList)
}
}
//上传chunk
functionupload({chunkList,chunk,fileMd5Value,i}){
current=0
letform=newFormData()
form.append("data",chunk)//切片流
form.append("total",chunkList.length)//总片数
form.append("index",i)//当前是第几片
form.append("fileMd5Value",fileMd5Value)
returnaxios({
method:'post',
url:BaseUrl+"/upload",
data:form
}).then(({data})=>{
if(data.stat){
current=current+1
//获取到上传的进度
constuploadPercent=Math.ceil((current/chunkList.length)*100)
}
})
}
所有切片上传完成后,再向后端发appreciate送一个上传完成的请求,即通知后端把所有切片进行合并,最终完成整个上传流程。
大功告成!由于篇幅有限,本文主要讲了前端的实现思路,最终落地成完整的项目,还是需要大家根据真实的项目需求来实现。
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~