前言
本文适合有一定node后端基础的前端同学。 如果你对后端完全不了解,请补上前置知识。
话不多说,让我们进入正题。
我们来看看各个版本的文件上传组件长什么样
等级函数
青铜 – 垃圾
原生+axios.post
白银-经验升级
粘贴、拖放、进度条
黄金 – 功能升级
断点续传、秒传、类型判断
白金-速度升级
网络工作者,时间切片,采样哈希
钻石 – 网络升级
异步并发控制,分片错误重试
国王-精雕细琢
慢启动控制、碎片整理等
1.最简单的文件上传
对于文件上传,我们需要获取文件对象node定时任务,然后使用formData发送给后台接收
function upload(file){
let formData = new FormData();
formData.append('newFile', file);
axios.post(
'http://localhost:8000/uploader/upload',
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
}
2.拖拽+粘贴+样式优化
懒得写了,可以在网上找库,网上什么都有,或者直接组件库解决问题
3.断点续传+秒传+进度条文件切片
我们把一个文件分成多个小块,保存在一个数组中,一个一个发送到后台,实现断点续传。
// 计算文件hash作为id
const { hash } = await calculateHashSample(file)
//todo 生成文件分片列表
// 使用file.slice()将文件切片
const fileList = [];
const count = Math.ceil(file.size / globalProp.SIZE);
const partSize = file.size / count;
let cur = 0 // 记录当前切片的位置
for (let i = 0; i < count; i++) {
let item = {
chunk: file.slice(cur, cur + partSize),
filename: `${hash}_${i}`
};
fileList.push(item);
}
计算散列
为了让后端知道这个分片是一个文件的一部分,以便聚合成一个完整的文件。 我们需要计算整个文件的唯一值(md5)作为分片的文件名。
// 通过input的event获取到file
<input type="file" @change="getFile">
// 使用SparkMD5计算文件hash,读取文件为blob,计算hash
let fileReader = new FileReader();
fileReader.onload = (e)=>{
let hexHash = SparkMD5.hash(e.target.result)
; console.log(hexHash);
};
断点续传+秒传(前端)
我们此时保存了一个100个文件切片的数组,可以通过遍历切片,不断向后端发送axios.post请求来设置开关,实现启动-暂停功能。
如果我们传递了 50 份并关闭浏览器怎么办?
这个时候就需要后台的配合了。 在上传文件之前,我们需要查看后台已经接收了多少文件。
当然,如果发现后台已经上传了这个文件,会直接显示上传完成(二传)
// 解构出已经上传的文件数组 文件是否已经上传完毕
// 通过文件hash和后缀查询当前文件有多少已经上传的部分
const {isFileUploaded, uploadedList} = await axios.get(
`http://localhost:8000/uploader/count
?hash=${hash}
&suffix=${fileSuffix}
`)
断点续传+秒传(后端)
至于后台的操作,比较简单
根据文件哈希创建文件夹并保存文件切片
查看文件上传状态,通过接口返回给前端
例如下面的文件切片文件夹
//! --------通过hash查询服务器中已经存放了多少份文件(或者是否已经存在文件)------
function checkChunks(hash, suffix) {
//! 查看已经存在多少文件 获取已上传的indexList
const chunksPath = `${uploadChunksDir}${hash}`;
const chunksList = (fs.existsSync(chunksPath) && fs.readdirSync(chunksPath)) || [];
const indexList = chunksList.map((item, index) =>item.split('_')[1])
//! 通过查询文件hash+suffix 判断文件是否已经上传
const filename = `${hash}${suffix}`
const fileList = (fs.existsSync(uploadFileDir) && fs.readdirSync(uploadFileDir)) || [];
const isFileUploaded = fileList.indexOf(filename) === -1 ? false : true
console.log('已经上传的chunks', chunksList.length);
console.log('文件是否存在', isFileUploaded);
return {
code: 200,
data: {
count: chunksList.length,
uploadedList: indexList,
isFileUploaded: isFileUploaded
}
}
}
进度条
实时计算上传成功的分片,自己实现是不行的。
4.采样hash和webWorker
因为在上传之前,我们需要计算文件的md5值,作为分片的id。
md5的计算是一件非常耗时的事情。 如果文件过大,js会卡在计算md5这一步,导致页面长时间卡顿。
这里我们提供三个优化思路
采样哈希(md5)
采样hash就是我们截取整个文件的一部分node定时任务,计算hash,提高计算速度。
1.我们将文件解析为二进制缓冲区数据,
2.从文件首尾提取2mb,中间每2mb提取2kb
3.将这些分片组合成新的buffer进行md5计算。
插图:
示例代码
//! ---------------抽样md5计算-------------------
function calculateHashSample(file) {
return new Promise((resolve) => {
//!转换文件类型(解析为BUFFER数据 用于计算md5)
const spark = new SparkMD5.ArrayBuffer();
const { size } = file;
const OFFSET = Math.floor(2 * 1024 * 1024); // 取样范围 2M
const reader = new FileReader();
let index = OFFSET;
// 头尾全取,中间抽2字节
const chunks = [file.slice(0, index)];
while (index < size) {
if (index + OFFSET > size) {
chunks.push(file.slice(index));
} else {
const CHUNK_OFFSET = 2;
chunks.push(file.slice(index, index + 2),
file.slice(index + OFFSET - CHUNK_OFFSET, index + OFFSET));
}
index += OFFSET;
}
// 将抽样后的片段添加到spark
reader.onload = (event) => {
spark.append(event.target.result);
resolve({
hash: spark.end(),//Promise返回hash
});
}
reader.readAsArrayBuffer(new Blob(chunks));
});
}
网络工作者
除了采样hash,我们还可以开一个webWorker线程来计算md5。
webWorker:为JS创建一个多线程的运行环境,让主线程创建工作线程,并将任务分配给后者。 在主线程运行的同时,工作线程也在运行,互不干扰。 工作线程运行后,将结果返回给主线程。 线。
具体用法请参考MDN
使用 Web Workers – Web API 接口参考 | MDN (mozilla.org)
时间片
熟悉React时间分片的同学也可以尝试一下,不过个人觉得这个方案不如上面两个。
不熟悉的同学可以自行查找,文章还是很多的。
这里不多讨论,仅提供思路
时间分片就是传说中的requestIdleCallback和requestAnimationCallback API,也可以通过更高层次的messageChannel进行封装。
Slice计算hash,每帧分配多个短任务,减少页面延迟。
5.文件类型判断
<input id="file" type="file" accept="image/*" />
const ext = file.name.substring(file.name.lastIndexOf('.') + 1);
复制代码
当然,这个限制是可以通过简单的修改文件扩展名来打破的,并不严谨。
我们将文件转换为二进制 blob。 文件的前几个字节表示文件类型,我们可以通过读取来判断。
例如下面的代码
// 判断是否为 .jpg
async function isJpg(file) {
// 截取前几个字节,转换为string
const res = await blobToString(file.slice(0, 3))
return res === 'FF D8 FF'
}
// 判断是否为 .png
async function isPng(file) {
const res = await blobToString(file.slice(0, 4))
return res === '89 50 4E 47'
}
// 判断是否为 .gif
async function isGif(file) {
const res = await blobToString(file.slice(0, 4))
return res === '47 49 46 38'
}
复制代码
当然,我们有现成的库可以做到这一点,比如文件类型库
文件类型 -npm (npmjs.com)[1]
6.异步并发控制(重要)
我们需要向后台上传多个文件分片,难道不能一个一个发送吗? 这里我们使用TCP并发+来实现上传的控制并发。
首先我们将100个文件分片封装为axios.post函数存放在任务池中
创建并发池,同时执行并发池中的任务,发送分片
设置计数器 i,当 i