一个企业级的文件上传组件应该是什么样的
目录
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}`
)
断点续传+秒传(后端)
至于后端的操作,就比较简单了
-
根据文件hash创建文件夹,保存文件切片
-
检查某文件的上传情况,通过接口返回给前端
例如以下文件切片文件夹

//! --------通过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函数,存入任务池中
-
创建一个并发池,同时执行并发池中的任务,发送片段
-
设置计数器i,当i<并发数时,才能将任务推入并发池
-
通过promise.race方法 最先执行完毕的请求会被返回 即可调用其.then方法 传入下一个请求(递归)
-
当最后一个请求发送完毕 向后端发起请求 合并文件片段
图解

代码
//! 传入请求列表 最大并发数 全部请求完毕后的回调
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.并发错误重试
-
使用catch捕获任务错误,上述axios.post任务执行失败后,重新把任务放到任务队列中
-
给每个任务对象设置一个tag,记录任务重试的次数
-
如果一个切片任务出错超过3次,直接reject。并且可以直接终止文件传输
8.慢启动控制
由于文件大小不一,我们每个切片的大小设置成固定的也有点略显笨拙,我们可以参考TCP协议的慢启动策略。设置一个初始大小,根据上传任务完成的时候,来动态调整下一个切片的大小, 确保文件切片的大小和当前网速匹配。
-
·chunk中带上size值,不过进度条数量不确定了,修改createFileChunk, 请求加上时间统计
-
·比如我们理想是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中的一个关键字,在不同的场合使用,this的值会发生变化,下面我将详细的介绍this在函数中的各种指向。 在方法中,this表示该方法所属的对象。 如果单独使用,this表示全局对象。 在函数中,this表示全…...
【触觉智能Purple Pi OH开发板体验】开箱体验:开源主板Purple Pi RK3566 上手指北
前言 前段时间收到来自【电子发烧友】的一款开发板,名叫:PurplePi,216G售价仅249元。它使用的芯片是rk3566,适配的OpenHarmony版本为3.2 Release 是目前最便宜的OpenHarmony标准系统开源开发板,并且软硬件全部开源&am…...
flink1.16使用消费/生产kafka之DataStream
flink高级版本后,消费kafka数据一种是Datastream 一种之tableApi。 上官网 Kafka | Apache Flink Kafka Source 引入依赖 flink和kafka的连接器,里面内置了kafka-client <dependency><groupId>org.apache.flink</groupId><arti…...
【多任务编程-线程通信】
进程/线程通信的方式 某些应用程序中,进程/进程和线程/线程之间不可避免的进行通信,进行消息传递,数据共享等 同一进程的线程之间通信方式包括Windows中常用Event, Message等。 不同进程之间的通信可以利用Event, FileMapping(内存共享), W…...
K8S暴露pod内多个端口
K8S暴露pod内多个端口 一、背景 公司统一用的某个底包跑jar服务,只暴露了8080端口 二、需求 由于有些服务在启动jar服务后,会启动多个端口,除了8080端口,还有别的端口需要暴露,我这里就还需要暴露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: 打开终端,运行以下命令以更新软件包列表: 安装RabbitMQ: 安装完成后,RabbitMQ服务会自动启动。你可以使用以下命令来检查RabbitMQ服务状态: 2.在CentOS…...
Kubernetes对象深入学习之四:对象属性编码实战
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是《Kubernetes对象深入学习》系列的第四篇,前面咱们读源码和文档,从理论上学习了kubernetes的对象相关的知识ÿ…...
深度学习入门教程(2):使用预训练模型来文字生成图片TextToImageGenerationWithNetwork
本深度学习入门教程是在polyu HPCStudio 启发以及资源支持下进行的,在此也感谢polyu以及提供支持的老师。 本文内容:在GoogleColab平台上使用预训练模型来文字生成图片Text To Image Generation With Network (1)你会学到什么&a…...
ORA-38760: This database instance failed to turn on flashback database
早晨接一个任务,使用rman备份在虚拟化单机上恢复实例,恢复参数文件、控制文件和数据文件都正常,recover归档时报错如下: Starting recover at 2023-07-28 10:25:01 using channel ORA_DISK_1 starting media recovery media reco…...
避免低级错误:深入解析Java的ConcurrentModificationException异常
在软件开发中,我们常常会遇到各种错误和异常。其中有一类比较低级但又常见的错误就是ConcurrentModificationException异常。最近了我就写了个这种异常,这个异常通常发生在使用迭代器遍历集合时,同时对集合进行修改,从而导致迭代器…...
7.28
1.思维导图 2.qt的sever #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器类 #include<QTcpSocket> //客户端类 #include<QMessageBox> //对话框类 #include<QList> …...
java线程中的常见方法(详解)
方法简介 方法名 功能 说明 start() 启动一个新线程,在新的线程运行 run 方法中的代码 start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如…...
线程池参数配置
上次面试被人问到,如果是IO 密集型的任务,该如何配置合适的线程数,当初我说要按照IO具体的请求毫秒时间,来配置具体的线程数。 NthreadsNcpu*(1w/c) 公式中 W/C 为系统 阻塞率 w:等待时间 c:计算时间一般情况下,如果存…...
Spread for Winform 16.2.20231.0 (SP2) Crack
Spread for Winform 16.2.20231.0 (SP2)发布。此版本包含针对客户报告的问题的重要修复: 安装版本 16 后,FarPoint.Localization.dll 将丢失。 将数据绑定到 Spread 时会出现 InvalidOperationException。 通过 Spread Designer 设置的上标将不会保留。…...
Go程序结构
Go程序结构 1、名称 名称的开头是一个字母或下划线,且区分大小写。 实体第一个字母的大小写决定其可见性是否跨包: 若名称以大写字母开头,它是导出的,对包外是可见和可访问的,可以被自己包以外的其他程序所引用…...
JAVA面试总结-Redis篇章(四)——双写一致性
JAVA面试总结-Redis篇章(四)——双写一致性 问:redis 做为缓存,mysql的数据如何与redis进行同步呢?第一种情况,如果你的项目一致性要求高的话 采用以下逻辑我们应该先删除缓存,再修改数据库&…...
赋能医院数字化转型,医院拍摄VR全景很有必要
医院有没有必要拍摄制作VR全景呢?近期也有合作商问我们这个问题,其实VR智慧医院是趋势、也是机遇。现在外面很多的口腔医院、医美机构等都开始引入VR全景技术了,力求打造沉浸式、交互式的VR智慧医院新体验,通过VR全景展示技术来助…...
Vue3项目中没有配置 TypeScript 支持,使用 TypeScript 语法
1.安装 TypeScript:首先,需要在项目中安装 TypeScript。在终端中运行以下命令 npm install typescript --save-dev2.创建 TypeScript 文件:在 Vue 3 项目中,可以创建一个以 .ts 后缀的文件,例如 MyComponent.ts。在这…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...
排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅
目录 前言 操作系统与驱动程序 是什么,为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中,我们在使用电子设备时,我们所输入执行的每一条指令最终大多都会作用到硬件上,比如下载一款软件最终会下载到硬盘上&am…...
消防一体化安全管控平台:构建消防“一张图”和APP统一管理
在城市的某个角落,一场突如其来的火灾打破了平静。熊熊烈火迅速蔓延,滚滚浓烟弥漫开来,周围群众的生命财产安全受到严重威胁。就在这千钧一发之际,消防救援队伍迅速行动,而豪越科技消防一体化安全管控平台构建的消防“…...
macOS 终端智能代理检测
🧠 终端智能代理检测:自动判断是否需要设置代理访问 GitHub 在开发中,使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新,例如: fatal: unable to access https://github.com/ohmyzsh/oh…...
