一个企业级的文件上传组件应该是什么样的
目录
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。在这…...

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

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

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

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

[Tools: Camera Conventions] NeRF中的相机矩阵估计
参考:NeRF代码解读-相机参数与坐标系变换 - 知乎 在NeRF中,一个重要的步骤是确定射线(rays)的初始点和方向。根据射线的初始点和方向,和设定射线深度和采样点数量,可以估计该射线成像的像素值。估计得到的…...
【sgUpload】自定义上传组件,支持上传文件夹及其子文件夹文件、批量上传,批量上传会有右下角上传托盘出现,支持本地上传图片转换为Base64image
特性: 支持批量上传文件、文件夹可自定义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开发中,有一些实践可以帮助进行内存优化,以减少应用程序的内存占用和提高性能。以下是一些常见的内存优化实践: 使用合适的数据结构和集合:选择合…...

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

【深度学习中常见的优化器总结】SGD+Adagrad+RMSprop+Adam优化算法总结及代码实现
文章目录 一、SGD,随机梯度下降1.1、算法详解1)MBSGD(Mini-batch Stochastic Gradient Descent)2)动量法:momentum3)NAG(Nesterov accelerated gradient)4)权重衰减项(we…...