前言

本文适合有一定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.断点续传+秒传+进度条文件切片

我们把一个文件分成多个小块,保存在一个数组中,一个一个发送到后台,实现断点续传。

node定时任务_火车头采集任务定时_java每天定时执行某个任务

// 计算文件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}
`
)

断点续传+秒传(后端)

至于后台的操作,比较简单

根据文件哈希创建文件夹并保存文件切片

查看文件上传状态,通过接口返回给前端

例如下面的文件切片文件夹

火车头采集任务定时_java每天定时执行某个任务_node定时任务

//! --------通过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计算。

插图:

java每天定时执行某个任务_火车头采集任务定时_node定时任务

示例代码

//! ---------------抽样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(03))
  return res === 'FF D8 FF'
}
// 判断是否为 .png 
async function isPng(file{
  const res = await blobToString(file.slice(04))
  return res === '89 50 4E 47'
}
// 判断是否为 .gif 
async function isGif(file{
  const res = await blobToString(file.slice(04))
  return res === '47 49 46 38'
}
复制代码

当然,我们有现成的库可以做到这一点,比如文件类型库

文件类型 -npm (npmjs.com)[1]

6.异步并发控制(重要)

我们需要向后台上传多个文件分片,难道不能一个一个发送吗? 这里我们使用TCP并发+来实现上传的控制并发。

java每天定时执行某个任务_node定时任务_火车头采集任务定时

首先我们将100个文件分片封装为axios.post函数存放在任务池中

创建并发池,同时执行并发池中的任务,发送分片

设置计数器 i,当 i