当前位置: 首页 > news >正文

一个企业级的文件上传组件应该是什么样的

目录

1.最简单的文件上传

2.拖拽+粘贴+样式优化

3.断点续传+秒传+进度条

文件切片

计算hash

断点续传+秒传(前端)

断点续传+秒传(后端)

进度条

4.抽样hash和webWorker

抽样hash(md5)

webWorker

时间切片

5.文件类型判断

通过文件头判断文件类型

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

7.并发错误重试

8.慢启动控制

9.碎片清理

后记

参考资料


本文适合有一定node后端基础的前端同学,如果对后端完全不了解请恶补前置知识。

废话不多说,直接进入正题。


我们来看一下,各个版本的文件上传组件大概都长什么样

等级功能
青铜-垃圾玩意原生+axios.post
白银-体验升级粘贴,拖拽,进度条
黄金-功能升级断点续传,秒传,类型判断
铂金-速度升级web-worker,时间切片,抽样hash
钻石-网络升级异步并发数控制,切片报错重试
王者-精雕细琢慢启动控制,碎片清理等等

1.最简单的文件上传

文件上传,我们需要获取文件对象,然后使用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);
}

计算hash

        为了让后端知道,这个切片是某个文件的一部分,以便聚合成一个完整的文件。我们需要计算完整file的唯一值(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}`
)

断点续传+秒传(后端)

至于后端的操作,就比较简单了

  1. 根据文件hash创建文件夹,保存文件切片

  2. 检查某文件的上传情况,通过接口返回给前端

例如以下文件切片文件夹

图片

//! --------通过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这一步,造成页面长时间卡顿。

我们这里提供三种思路进行优化:

抽样hash(md5)

        抽样hash是指,我们截取整个文件的一部分,计算hash,提升计算速度。

1. 我们将file解析为二进制buffer数据,

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); // 取样范围 2Mconst 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;}// 将抽样后的片段添加到sparkreader.onload = (event) => {spark.append(event.target.result);resolve({hash: spark.end(),//Promise返回hash});}reader.readAsArrayBuffer(new Blob(chunks));});
}

webWorker

        除了抽样hash,我们可以另外开启一个webWorker线程去专门计算md5。

        webWorker: 就是给JS创造多线程运行环境,允许主线程创建worker线程,分配任务给后者,主线程运行的同时worker线程也在运行,相互不干扰,在worker线程运行结束后把结果返回给主线程。

具体使用方式可以参考MDN或者其他文章:

        使用 Web Workers \- Web API 接口参考 | MDN \(mozilla.org\)[1]

        一文彻底学会使用web worker \- 掘金 \(juejin.cn\)[2]

时间切片

        熟悉React时间切片的同学也可以去试一试,不过个人认为这个方案没有以上两种好。

        不熟悉的同学可以自行掘金一下,文章还是很多的。这里就不多做论述,只提供思路。

        时间切片也就是传说中的requestIdleCallback,requestAnimationFrame 这两个API了,或者高级一点自己通过messageChannel去封装。

        切片计算hash,将多个短任务分布在每一帧里,减少页面卡顿。

5.文件类型判断

简单一点,我们可以通过input标签的accept属性,或者截取文件名来判断类型

<input id="file" type="file" accept="image/*" />const ext = file.name.substring(file.name.lastIndexOf('.') + 1);

当然这种限制可以简单的通过修改文件后缀名来突破,并不严谨。

通过文件头判断文件类型

我们将文件转化为二进制blob,文件的前几个字节就表示了文件类型,我们读取进行判断即可。

比如如下代码

// 判断是否为 .jpg 
async function isJpg(file) {// 截取前几个字节,转换为stringconst 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'
}

当然咱们有现成的库可以做这件事情,比如 file-type 这个库

        file-type \- npm \(npmjs.com\)[3]

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

        我们需要将多个文件片段上传给后端,总不能一个个发送把?我们这里使用TCP的并发+实现控制并发进行上传。

图片

         首先我们将100个文件片段都封装为axios.post函数,存入任务池中

  1. 创建一个并发池,同时执行并发池中的任务,发送片段

  2. 设置计数器i,当i<并发数时,才能将任务推入并发池

  3. 通过promise.race方法 最先执行完毕的请求会被返回 即可调用其.then方法 传入下一个请求(递归)

  4. 当最后一个请求发送完毕 向后端发起请求 合并文件片段

图解

图片

代码

//! 传入请求列表  最大并发数  全部请求完毕后的回调
function concurrentSendRequest(requestArr: any, max = 3, callback: any) {let i = 0 // 执行任务计数器let concurrentRequestArr: any[] = [] //并发请求列表let toFetch: any = () => {// (每次执行i+1) 如果i=arr.length 说明是最后一个任务  // 返回一个resolve 作为最后的toFetch.then()执行// (执行Promise.all() 全部任务执行完后执行回调函数  发起文件合并请求)if (i === requestArr.length) {return Promise.resolve()}//TODO 执行异步任务  并推入并发列表(计数器+1)let it = requestArr[i++]()concurrentRequestArr.push(it)//TODO 任务执行后  从并发列表中删除it.then(() => {concurrentRequestArr.splice(concurrentRequestArr.indexOf(it), 1)})//todo 如果并发数达到最大数,则等其中一个异步任务完成再添加let p = Promise.resolve()if (concurrentRequestArr.length >= max) {//! race方法 返回fetchArr中最快执行的任务结果 p = Promise.race(concurrentRequestArr)}//todo race中最快完成的promise,在其.then递归toFetch函数if (globalProp.stop) { return p.then(() => { console.log('停止发送') }) }return p.then(() => toFetch())}// 最后一组任务全部执行完再执行回调函数(发起合并请求)(如果未合并且未暂停)toFetch().then(() =>Promise.all(concurrentRequestArr).then(() => {if (!globalProp.stop && !globalProp.finished) { callback() }}))
}

7.并发错误重试

  1. 使用catch捕获任务错误,上述axios.post任务执行失败后,重新把任务放到任务队列中

  2. 给每个任务对象设置一个tag,记录任务重试的次数

  3. 如果一个切片任务出错超过3次,直接reject。并且可以直接终止文件传输

8.慢启动控制

        由于文件大小不一,我们每个切片的大小设置成固定的也有点略显笨拙,我们可以参考TCP协议的慢启动策略。设置一个初始大小,根据上传任务完成的时候,来动态调整下一个切片的大小, 确保文件切片的大小和当前网速匹配。

  1. ·chunk中带上size值,不过进度条数量不确定了,修改createFileChunk, 请求加上时间统计

  2. ·比如我们理想是30秒传递一个。初始大小定为1M,如果上传花了10秒,那下一个区块大小变成3M。如果上传花了60秒,那下一个区块大小变成500KB 以此类推

9.碎片清理

        如果用户上传文件到一半终止,并且以后也不传了,后端保存的文件片段也就没有用了。

        我们可以在node端设置一个定时任务setInterval,每隔一段时间检查并清理不需要的碎片文件。

        可以使用 node-schedule 来管理定时任务,比如每天检查一次目录,如果文件是一个月前的,那就直接删除把。

垃圾碎片文件

图片

后记

        以上就是一个完整的比较高级的文件上传组件的全部功能,希望各位有耐心看到这里的新手小伙伴能够融会贯通。每天进步一点点。

参考资料

[1] https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers: https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWeb_Workers_API%2FUsing_web_workers

[2] https://juejin.cn/post/7139718200177983524: https://juejin.cn/post/7139718200177983524

[3] https://www.npmjs.com/package/file-type: https://link.juejin.cn?target=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Ffile-type

相关文章:

一个企业级的文件上传组件应该是什么样的

目录 1.最简单的文件上传 2.拖拽粘贴样式优化 3.断点续传秒传进度条 文件切片 计算hash 断点续传秒传(前端) 断点续传秒传(后端) 进度条 4.抽样hash和webWorker 抽样hash(md5) webWorker 时间切片 5.文件类型判断 通过文件头判断文件类型 6.异步并发数控制(重要…...

安全渗透重点内容

this是js中的一个关键字&#xff0c;在不同的场合使用&#xff0c;this的值会发生变化&#xff0c;下面我将详细的介绍this在函数中的各种指向。 在方法中&#xff0c;this表示该方法所属的对象。 如果单独使用&#xff0c;this表示全局对象。 在函数中&#xff0c;this表示全…...

【触觉智能Purple Pi OH开发板体验】开箱体验:开源主板Purple Pi RK3566 上手指北

前言 前段时间收到来自【电子发烧友】的一款开发板&#xff0c;名叫&#xff1a;PurplePi&#xff0c;216G售价仅249元。它使用的芯片是rk3566&#xff0c;适配的OpenHarmony版本为3.2 Release 是目前最便宜的OpenHarmony标准系统开源开发板&#xff0c;并且软硬件全部开源&am…...

flink1.16使用消费/生产kafka之DataStream

flink高级版本后&#xff0c;消费kafka数据一种是Datastream 一种之tableApi。 上官网 Kafka | Apache Flink Kafka Source 引入依赖 flink和kafka的连接器&#xff0c;里面内置了kafka-client <dependency><groupId>org.apache.flink</groupId><arti…...

【多任务编程-线程通信】

进程/线程通信的方式 某些应用程序中&#xff0c;进程/进程和线程/线程之间不可避免的进行通信&#xff0c;进行消息传递&#xff0c;数据共享等 同一进程的线程之间通信方式包括Windows中常用Event, Message等。 不同进程之间的通信可以利用Event, FileMapping(内存共享), W…...

K8S暴露pod内多个端口

K8S暴露pod内多个端口 一、背景 公司统一用的某个底包跑jar服务&#xff0c;只暴露了8080端口 二、需求 由于有些服务在启动jar服务后&#xff0c;会启动多个端口&#xff0c;除了8080端口&#xff0c;还有别的端口需要暴露&#xff0c;我这里就还需要暴露9999端口。 注&a…...

Python 列表

""" #list函数 ls list() #创建一个空列表 print(list()) print(list(str(1234)))#[1, 2, 3, 4] print(list(range(5)))#[0, 1, 2, 3, 4] print(list((1,2,3,4)))#[1, 2, 3, 4] print(list(Lift is short, you need python))#注意空格也算一个字符 #[L, i, f,…...

Rabbitmq的安装与使用(Linux版)

目录 Rabbitmq安装 1.在Ubuntu上安装RabbitMQ&#xff1a; 打开终端&#xff0c;运行以下命令以更新软件包列表&#xff1a; 安装RabbitMQ&#xff1a; 安装完成后&#xff0c;RabbitMQ服务会自动启动。你可以使用以下命令来检查RabbitMQ服务状态&#xff1a; 2.在CentOS…...

Kubernetes对象深入学习之四:对象属性编码实战

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 本文是《Kubernetes对象深入学习》系列的第四篇&#xff0c;前面咱们读源码和文档&#xff0c;从理论上学习了kubernetes的对象相关的知识&#xff…...

深度学习入门教程(2):使用预训练模型来文字生成图片TextToImageGenerationWithNetwork

本深度学习入门教程是在polyu HPCStudio 启发以及资源支持下进行的&#xff0c;在此也感谢polyu以及提供支持的老师。 本文内容&#xff1a;在GoogleColab平台上使用预训练模型来文字生成图片Text To Image Generation With Network &#xff08;1&#xff09;你会学到什么&a…...

ORA-38760: This database instance failed to turn on flashback database

早晨接一个任务&#xff0c;使用rman备份在虚拟化单机上恢复实例&#xff0c;恢复参数文件、控制文件和数据文件都正常&#xff0c;recover归档时报错如下&#xff1a; Starting recover at 2023-07-28 10:25:01 using channel ORA_DISK_1 starting media recovery media reco…...

避免低级错误:深入解析Java的ConcurrentModificationException异常

在软件开发中&#xff0c;我们常常会遇到各种错误和异常。其中有一类比较低级但又常见的错误就是ConcurrentModificationException异常。最近了我就写了个这种异常&#xff0c;这个异常通常发生在使用迭代器遍历集合时&#xff0c;同时对集合进行修改&#xff0c;从而导致迭代器…...

7.28

1.思维导图 2.qt的sever #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器类 #include<QTcpSocket> //客户端类 #include<QMessageBox> //对话框类 #include<QList> …...

java线程中的常见方法(详解)

方法简介 方法名 功能 说明 start() 启动一个新线程&#xff0c;在新的线程运行 run 方法中的代码 start 方法只是让线程进入就绪&#xff0c;里面代码不一定立刻运行&#xff08;CPU 的时间片还没分给它&#xff09;。每个线程对象的start方法只能调用一次&#xff0c;如…...

线程池参数配置

上次面试被人问到&#xff0c;如果是IO 密集型的任务&#xff0c;该如何配置合适的线程数&#xff0c;当初我说要按照IO具体的请求毫秒时间&#xff0c;来配置具体的线程数。 NthreadsNcpu*(1w/c) 公式中 W/C 为系统 阻塞率 w:等待时间 c:计算时间一般情况下&#xff0c;如果存…...

Spread for Winform 16.2.20231.0 (SP2) Crack

Spread for Winform 16.2.20231.0 (SP2)发布。此版本包含针对客户报告的问题的重要修复&#xff1a; 安装版本 16 后&#xff0c;FarPoint.Localization.dll 将丢失。 将数据绑定到 Spread 时会出现 InvalidOperationException。 通过 Spread Designer 设置的上标将不会保留。…...

Go程序结构

Go程序结构 1、名称 ​ 名称的开头是一个字母或下划线&#xff0c;且区分大小写。 实体第一个字母的大小写决定其可见性是否跨包&#xff1a; ​ 若名称以大写字母开头&#xff0c;它是导出的&#xff0c;对包外是可见和可访问的&#xff0c;可以被自己包以外的其他程序所引用…...

JAVA面试总结-Redis篇章(四)——双写一致性

JAVA面试总结-Redis篇章&#xff08;四&#xff09;——双写一致性 问&#xff1a;redis 做为缓存&#xff0c;mysql的数据如何与redis进行同步呢&#xff1f;第一种情况&#xff0c;如果你的项目一致性要求高的话 采用以下逻辑我们应该先删除缓存&#xff0c;再修改数据库&…...

赋能医院数字化转型,医院拍摄VR全景很有必要

医院有没有必要拍摄制作VR全景呢&#xff1f;近期也有合作商问我们这个问题&#xff0c;其实VR智慧医院是趋势、也是机遇。现在外面很多的口腔医院、医美机构等都开始引入VR全景技术了&#xff0c;力求打造沉浸式、交互式的VR智慧医院新体验&#xff0c;通过VR全景展示技术来助…...

Vue3项目中没有配置 TypeScript 支持,使用 TypeScript 语法

1.安装 TypeScript&#xff1a;首先&#xff0c;需要在项目中安装 TypeScript。在终端中运行以下命令 npm install typescript --save-dev2.创建 TypeScript 文件&#xff1a;在 Vue 3 项目中&#xff0c;可以创建一个以 .ts 后缀的文件&#xff0c;例如 MyComponent.ts。在这…...

数据可视化大屏拼接屏开发实录:屏幕分辨率测试工具

一、可视化大屏开发 在数据可视化大屏开发时&#xff0c;确定数据可视化大屏拼接屏的板块尺寸需要考虑以下几个因素&#xff1a; 屏幕分辨率&#xff1a;首先需要知道每个板块屏幕的分辨率&#xff0c;包括宽度和高度&#xff0c;这决定了每个板块上可以显示的像素数量。 数据…...

每日一题7.28 209

209. 长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0 。 本题应该是用前缀…...

Python + Playwright 无头浏览器Chrome找不到元素

用Python Playwright调试时&#xff0c;发现不用无头浏览器&#xff08;即headlessFalse&#xff09;代码能够运行成功&#xff0c;但是一用无头浏览器时&#xff08;即headlessTrue&#xff09;就会报错&#xff0c;提示找不到元素。换成Firefox浏览器又不会有这个问题&#…...

C++信号量与共享内存实现进程间通信

关于信号量和共享内存的相关知识可参考下面链接&#xff1a; 进程间通信方式介绍_夜雨听萧瑟的博客-CSDN博客 C 创建共享内存_c共享内存_夜雨听萧瑟的博客-CSDN博客 信号量SytemV与Posix信号量的介绍与用法_夜雨听萧瑟的博客-CSDN博客 直接上代码&#xff0c;代码如下&#…...

[Tools: Camera Conventions] NeRF中的相机矩阵估计

参考&#xff1a;NeRF代码解读-相机参数与坐标系变换 - 知乎 在NeRF中&#xff0c;一个重要的步骤是确定射线&#xff08;rays&#xff09;的初始点和方向。根据射线的初始点和方向&#xff0c;和设定射线深度和采样点数量&#xff0c;可以估计该射线成像的像素值。估计得到的…...

【sgUpload】自定义上传组件,支持上传文件夹及其子文件夹文件、批量上传,批量上传会有右下角上传托盘出现,支持本地上传图片转换为Base64image

特性&#xff1a; 支持批量上传文件、文件夹可自定义headers可自定义过滤上传格式可自定义上传API接口支持drag属性开启可拖拽上传文件、文件夹 sgUpload源码 <template><div :class"$options.name" :dragenter"isDragenter"><!-- 上传按钮…...

Kafka 实时处理Stream与Batch的对比分析

Kafka 实时处理Stream与Batch的对比分析 一、简介1. Kafka的定义和特点2. Kafka实时处理基础架构 二、Stream和Batch1. Stream和Batch的区别2. 对比Stream和Batch的优缺点Stream的优缺点Batch的优缺点 三、使用场景1. 使用场景对比Batch使用场景Stream使用场景 2. 如何选择Stre…...

Andriod开发性能优化实践

文章目录 内存优化布局优化网络优化图片优化内存泄露绘制优化 内存优化 在Android开发中&#xff0c;有一些实践可以帮助进行内存优化&#xff0c;以减少应用程序的内存占用和提高性能。以下是一些常见的内存优化实践&#xff1a; 使用合适的数据结构和集合&#xff1a;选择合…...

linux环境安装mysql数据库

一&#xff1a;查看是否自带mariadb数据库 命令&#xff1a;rpm -qa | grep mariadb 如果自带数据库则卸载掉重新安装 命令&#xff1a;yum remove mariadb-connector-c-3.1.11-2.el8_3.x86_64 二&#xff1a;将压缩文件上传到/user/local/mysql文件夹 或者直接下载 命令&a…...

【深度学习中常见的优化器总结】SGD+Adagrad+RMSprop+Adam优化算法总结及代码实现

文章目录 一、SGD&#xff0c;随机梯度下降1.1、算法详解1&#xff09;MBSGD&#xff08;Mini-batch Stochastic Gradient Descent&#xff09;2&#xff09;动量法&#xff1a;momentum3&#xff09;NAG(Nesterov accelerated gradient)4&#xff09;权重衰减项&#xff08;we…...