Vue3 + Nodejs 实战 ,文件上传项目--大文件分片上传+断点续传
目录
1.大文件上传的场景
2.前端实现
2.1 对文件进行分片
2.2 生成hash值(唯一标识)
2.3 发送上传文件请求
3.后端实现
3.1 接收分片数据临时存储
3.2 合并分片
4.完成段点续传
4.1修改后端
4.2 修改前端
5.测试
博客主页:専心_前端,javascript,mysql-CSDN博客
系列专栏:vue3+nodejs 实战--文件上传
前端代码仓库:jiangjunjie666/my-upload: vue3+nodejs 上传文件的项目,用于学习 (github.com)
后端代码仓库:jiangjunjie666/my-upload-server: nodejs上传文件的后端 (github.com)
欢迎关注
在上一篇中我们实现了文件的拖拽上传,Vue3 + Nodejs 实战 ,文件上传项目--实现拖拽上传-CSDN博客
该篇就讲讲大文件上传的需求场景
1.大文件上传的场景
如果遇到需要上传电影或者视频之类的需求,那么上传的文件是非常大的,这个时候我们不能说用一个请求就直接将所有的文件传输过去,因为这个大文件上传时间相比较来说是比较长的,存在很多的弊端,假如用户刷新了页面之类的情况,这时候上传又需要重头开始上传,这对用户以及服务器都是不妥的。
需要解决的基本问题(其他的扩展根据需求来定)
- 对大文件进行分片上传
- 上传的实时进度显示
- 上传中断后再次上传跳过已经上传的部分
2.前端实现
2.1 对文件进行分片
对文件进行分片我们用到的是 file.slice(i,j) 这个方法
其中的参数代表的意思就是切割的大小,以size单位的,i-j 就是切这一段的数据
<template><input type="file" @change="fileChange" />
</template><scritp>
import { ref } from 'vue'
const fileList = ref([])
const file = ref([])
const fileChange = (e) => {console.log(e.target.files[0])//分片file.value = e.target.files[0]for (let i = 0; i < file.value.size; i += 1024 * 1024) {fileList.value.push(file.value.slice(i, i + 1024 * 1024))}
}
</script>
这样就将文件切成了每个大小为 1024 * 1024 的大小的文件,也就是1mb
2.2 生成hash值(唯一标识)
因为我们进行分片后的文件数据并不能单独成为一个原本的文件,一般都是二进制数据,我们在分片上传的时候需要临时存储已经上传的文件,这时候为了能对应到每个文件,需要对每个文件生成对应的文件hash值,这里需要用到hash算法,项目中大家可以直接使用spark-md5这个库生成文件hash。
总之文件hash的作用就是生成唯一的标识,该算法生成的hash值永远不会出现重复 ,但是同一文件多次生成的都是一样的。
安装 spark-md5
npm i spark-md5
引入生成hash
import SparkMD5 from 'spark-md5'const fileMd5 = ref('')const hash = new SparkMD5.ArrayBuffer() // 构建hash值对象const fileReader = new FileReader()fileReader.onload = () => {hash.append(fileReader.result)fileMd5.value = hash.end()}fileReader.readAsArrayBuffer(file.value)
2.3 发送上传文件请求
大文件分片上传一般都是二个请求,一个是分片上传的请求,一个是分片上传完之后的合并请求。
分片上传:
const upload = async (index) => {if (index == fileList.value.length) {mergeUpload()return}const formData = new FormData()formData.append('chunk', fileList.value[index])formData.append('index', index)formData.append('name', fileMd5.value + '@' + index) // 临时的二进制文件分片formData.append('filename', fileMd5.value) // 文件名,采用hashlet res = await http.post('/api/upload_chunk1', formData)if(res.code == 200){upload(index+1)}else{upload(index) // 失败重新上传 }
}
合并分片:
const mergeUpload = async () => {//合并请求let res = await http.post('/api/merge_chunk', {filename: fileMd5.value, //最后合并的文件名extname: file.value.type.split('/').pop() //文件后缀})if ((res.code = 200)) {file.value = nullfileList.value = []fileMd5.value = ''ElMessage({type: 'success',message: '上传成功'})}
}
3.后端实现
我这边使用的是 multiparty 中间件完成的该需求,可以安装一下
npm i multiparty
如果没有看过建的项目,可以看看第一期喔:Vue3 + Nodejs 实战 ,文件上传项目--实现图片上传-CSDN博客
3.1 接收分片数据临时存储
首先要做的就是利用请求中携带的数据进行创建一个临时保存分片数据的目录,目录名可以使用hash值,这样可以做到唯一性,并且后面实现断点续传也能更方便。
分片上传的数据会被临时保存在 Temp 文件夹中,如果windows电脑的话,可以 win + R ,输入 %temp% 就能进入该目录,里面保存的都是临时文件。我的做法就是将这个的文件copy一份到对应的目录下,然后删除临时文件。
const multiparty = require('multiparty')
const path = require('path')
const fs = require('fs')
exports.upload_chunk1 = (req, res, next) => {// 二进制数据上传const form = new multiparty.Form()form.parse(req, (err, fields, files) => {if (err) {next(err)return}//将每一次上传的数据进行统一的存储const oldName = files.chunk[0].pathconst newName = path.join(__dirname, '../../public/upload/chunk/' + fields['filename'][0] + '/' + fields['name'][0])//创建临时存储目录fs.mkdirSync('./public/upload/chunk/' + fields['filename'][0], {recursive: true})console.log(fields)console.log(files)fs.copyFile(oldName, newName, (err) => {if (err) {console.error(err)} else {// 删除源文件fs.unlink(oldName, (err) => {if (err) {console.error(err)} else {console.log('文件复制和删除成功')}})}})res.send({code: 200,msg: '分片上传成功'})})
}
这是刚刚上传的一批二进制文件
3.2 合并分片
合并接口:
exports.merge_chunk = (req, res, next) => {const fields = req.bodyconsole.log(fields)thunkStreamMerge('../../public/upload/chunk/' + fields.filename, '../../public/upload/' + fields.filename + '.' + fields.extname)res.send({code: 200,data: '/public/upload/' + fields.filename + '.' + fields.extname})
}
编写一个thunkStreamMerge
函数用来合并分块文件。它接收两参数:源文件目录 sourceFiles
和目标文件路径 targetFile
。利用 fs.readdirSync
读取源文件目录下的文件列表,并根据文件名中的序号排序,以确保按正确的顺序合并文件。接着创建一个可写流 (fileWriteStream
),用于将所有分块文件的内容写入到目标文件中。这个目标文件路径是由 targetFile
指定的。
// 文件合并
function thunkStreamMerge(sourceFiles, targetFile) {const chunkFilesDir = path.join(__dirname, sourceFiles)const chunkTargetDir = path.join(__dirname, targetFile)const list = fs.readdirSync(chunkFilesDir) //读取目录中的文件const fileList = list.sort((a, b) => a.split('@')[1] * 1 - b.split('@')[1] * 1).map((name) => ({name,filePath: path.resolve(chunkFilesDir, name)}))const fileWriteStream = fs.createWriteStream(chunkTargetDir)thunkStreamMergeProgress(fileList, fileWriteStream, chunkFilesDir)
}
thunkStreamMergeProgress
函数是用来处理每个分块文件的函数。它接收三个参数:fileList
,包含了分块文件信息的数组;fileWriteStream
,目标文件的可写流;以及可选的 sourceFiles
,用于删除临时文件目录。
首先,函数检查 fileList
是否为空,如果为空,表示所有分块文件已经合并完成。此时,它向目标文件写入一个标识('完成了'),并根据需要删除源文件目录 sourceFiles
。
如果 fileList
不为空,函数会从中取出第一个文件信息,并获取其文件路径。然后,它创建一个可读流 (currentReadStream
) 来读取该分块文件的内容。
接下来,函数使用 .pipe
方法将当前可读流的内容传输到目标文件的可写流中,这将逐步将分块文件的内容写入目标文件。
当当前可读流读取完毕(end
事件触发)后,它递归调用 thunkStreamMergeProgress
函数,处理下一个分块文件,直到所有分块文件都合并到了目标文件中。
//合并每一个分片
function thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles) {if (!fileList.length) {// thunkStreamMergeProgress(fileList)fileWriteStream.end('完成了')// 删除临时目录if (sourceFiles) fs.rmdirSync(sourceFiles, { recursive: true, force: true })return}const data = fileList.shift() // 取第一个数据const { filePath: chunkFilePath } = dataconst currentReadStream = fs.createReadStream(chunkFilePath) // 读取文件// 把结果往最终的生成文件上进行拼接currentReadStream.pipe(fileWriteStream, { end: false })currentReadStream.on('end', () => {// console.log(chunkFilePath);// 拼接完之后进入下一次循环thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles)})
}
这样做完后就能实现基本的文件分片上传以及合并分片了
这是上传的一个视频
4.完成段点续传
4.1修改后端
断点续传的情况就是文件上传至一半时突然做了其他的交互让上传停止了,那么下次再上传时就需要重新开始从头上传,这样非常消耗时间,对用户的体验与服务器的维护都不友好,这时候就要判断是否有对应的文件数据,直接将对应的索引传给客户端,客户端直接从该分片开始传输即可,因为我们之前给每个临时文件都加了 hash+@+index,我们可以读取文件夹目录查找最后的index传输给客户端即可。
当然这种解决方法是因为我没有采用数据库的情况,如果采用数据库或者其他的需求,基本的实现思路都差不多,利用唯一值 hash 进行传输。
后端接口加上一个判断,是否为段点传输
exports.upload_chunk1 = (req, res, next) => {// 二进制数据上传const form = new multiparty.Form()form.parse(req, (err, fields, files) => {if (err) {next(err)return}let pa = path.join(__dirname, '../../public/upload/chunk/' + fields['filename'][0])console.log(pa)//判断是否为断点续传if (fs.existsSync(pa) && parseInt(fields.index[0]) === 0) {//存在该目录//返回最大的索引let maxIndex = 0let arr = fs.readdirSync(pa)for (let i = 0; i < arr.length; i++) {let str = parseInt(arr[i].split('@')[1])console.log(str)if (str > maxIndex) {maxIndex = str}}res.send({code: 300,msg: '存在该目录,请继续上传',index: maxIndex})} else {//将每一次上传的数据进行统一的存储const oldName = files.chunk[0].pathconst newName = path.join(__dirname, '../../public/upload/chunk/' + fields['filename'][0] + '/' + fields['name'][0])//创建临时存储目录fs.mkdirSync('./public/upload/chunk/' + fields['filename'][0], {recursive: true})console.log(fields)console.log(files)fs.copyFile(oldName, newName, (err) => {if (err) {console.error(err)} else {// 删除源文件fs.unlink(oldName, (err) => {if (err) {console.error(err)} else {console.log('文件复制和删除成功')}})}})res.send({code: 200,msg: '分片上传成功'})}})
}
4.2 修改前端
前端加上进度条,发送请求时对相应的数据进行判断即可完成此需求了
分片请求,合并请求不变
const upload = async (index) => {if (index == fileList.value.length) {mergeUpload()return}const formData = new FormData()formData.append('chunk', fileList.value[index])formData.append('index', index)formData.append('name', fileMd5.value + '@' + index) // 名字formData.append('filename', fileMd5.value) // 文件名let res = await http.post('/api/upload_chunk1', formData)console.log(res)if (res.code == 300) {//证明已经存在部分文件percentage.value = ((res.index / fileList.value.length) * 100).toFixed(2)upload(res.index + 1)} else if (res.code == 200) {percentage.value = (((index + 1) / fileList.value.length) * 100).toFixed(2)upload(index + 1)} else {upload(index)}
}
加上进度条
<template><div><input type="file" @change="fileChange" /><div class="progress"><el-progress :text-inside="true" :stroke-width="24" :percentage="percentage" status="success" /></div></div>
</template><script>let percentage = ref(0)</script>
5.测试
我这里选择一个 624Mb的视频
我在上传进度到一半时刷新了页面
分片文件也只有450个
这时候重新选择该视频重新上传
这时候进度条很快就能到对应的进度
后端也没有上传多余的文件,直接接着传输,速度很快就传输完并且合并成了视频
到这基本的分片传输 + 断点续传就实现了,可能还存在者一些小问题,这个大家再项目中根据需求来做相应的改变。
具体的详细代码请大家到仓库下载,或者可以去我的主页资源中下载源码。
相关文章:

Vue3 + Nodejs 实战 ,文件上传项目--大文件分片上传+断点续传
目录 1.大文件上传的场景 2.前端实现 2.1 对文件进行分片 2.2 生成hash值(唯一标识) 2.3 发送上传文件请求 3.后端实现 3.1 接收分片数据临时存储 3.2 合并分片 4.完成段点续传 4.1修改后端 4.2 修改前端 5.测试 博客主页:専心_前端…...

宏(预编译)详解
目录 一、程序的编译环境 二、运行环境 三、预编译详解 3.1预定义符号 3.2.1 #define 定义标识符 3.2.2 #define 定义宏 3.2.3#define替换规则 3.2.4 #和## 2)##的作用: 3.2.5宏和函数的对比 3.2.6宏的命名约定和#undef指令 一、命名约定: …...

hue实现对hiveserver2 的负载均衡
如果你使用的是CDH集群那就很是方便的 在Cloudera Manager中,进入HDFS Service 进入Instances标签页面,点击Add Role Instances按钮,如下图所示 点击Continue按钮,如下图所示 返回Instances页面,选择HttpFS角色…...

SkyWalking 告警规则配置说明
Skywalking告警功能是在6.x版本新增的,其核心由一组规则驱动,这些规则定义在config/alarm-settings.yml 文件中。告警规则定义分为两部分: 1、告警规则:它们定义了应该如何触发度量警报,应该考虑什么条件 2、webhook(网络钩子):定义当告警触发时,哪些服务终端需要被…...

HTML 表单笔记/练习
表单 概述 表单用于收集用户信息,用户填写表单提交到服务器 一般传参方式: GETPOSTCookie 传参要素 传参方式 GETPOST 参数的名字目标页面内容的数据类型(只有在上传文件的时候) 提示信息 一个表单中通常还包含一些说明性的文…...

关于Java Integer和Long使用equals直接比较
Integer和Long不能直接equals比较会返回False Long.class源码 public boolean equals(Object obj) {if (obj instanceof Long) {return this.value (Long)obj;} else {return false;} }Integer.class源码 public boolean equals(Object obj) {if (obj instanceof Integer) {…...

nodejs+vue衣服穿搭推荐系统-计算机毕业设计
模块包括主界面,系统首页、个人中心、用户管理、风格标签管理、衣服分类管理、衣服穿搭管理、服装信息管理、我的搭配管理、用户反馈、系统管理等进行相应的操作。无论是日常生活,还是特定场景,诸如面试、约会等,人们都有展现自我…...

Java并发面试题:(七)ThreadLocal原理和内存泄漏
ThreadLocal是什么? ThreadLocal是线程本地存储机制,可以将数据缓存在线程内部。ThreadLocal存储的变量在线程内共享的,在线程间又是隔离的。 ThreadLocal实现原理? ThreadLocal的底层是ThreadLocalMap,每个Thread都…...

香港服务器在国内访问太慢怎么能提高?
一直以来,全球化业务需求的增长是跟随着蓬勃向上的互联网而发展的。有了网络,海外贸易就在鼠标的轻点中完成。而IDC市场中的香港服务器也因为免备案政策的特性,开始逐渐成为企业想要跨越地域壁垒而考虑的对象。但在使用过程中ÿ…...

使用Proxyman抓取Android的https请求
使用Proxyman抓取Android的https请求 有时,您可能需要测试您的移动应用程序并检查与其关联的所有网络请求。在网络上,此任务非常简单,只需按Ctrl Shift I打开开发人员工具即可。从那里,您可以导航到网络选项卡并检查与网页相关的…...

基础MySQL的语法练习
基础MySQL的语法练习 create table DEPT(DEPTNO int(2) not null,DNAME VARCHAR(14),LOC VARCHAR(13) );alter table DEPTadd constraint PK_DEPT primary key (DEPTNO);create table EMP (EMPNO int(4) primary key,ENAME VARCHAR(10),JOB VARCHAR(9),MGR …...

RAID和LVM配置指南:创建、扩容和管理RAID设备和逻辑卷的方法
文章目录 1. 简介1.1 什么是RAID和LVM1.2 RAID和LVM的作用和优势 2. RAID配置命令:mdadm2.1 安装mdadm2.2 创建RAID设备2.2.1 RAID 02.2.2 RAID 12.2.3 RAID 52.2.4 RAID 10 2.3 添加磁盘到RAID设备2.4 删除磁盘从RAID设备2.5 查看和管理RAID设备2.6 故障处理与恢复…...

MapStruct使用方法
一、用途 1.1 优势 与动态映射框架相比,MapStruct 具有以下优势: (1)通过使用普通方法getter、setter调用,而不是反射来快速执行,效率很高。 (2)编译时类型安全:只能映…...

【LeetCode】50. Pow(x, n)
1 问题 实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即, x n x^n xn )。 示例 1: 输入:x 2.00000, n 10 输出:1024.00000 示例 2: 输入:x 2.10000, n 3 输出&a…...

vue2技能树(2)-模板语法、vue的工具链、渐进式框架
目录 Vue2技能树Vue 2 简单的模板语法详解插值绑定属性指令v-if 和 v-elsev-forv-on 计算属性过滤器插槽 Vue 2 生态系统详解1. Vue Router2. Vuex3. Vue CLI4. Axios5. Vue Devtools6. Element UI、Vuetify、Quasar等UI框架7. Nuxt.js8. Vue Apollo、Vue Router、Vue Fire等插…...

【Git系列教程-目录大纲】
《Git系列教程-目录大纲》 完完全全从零开始深入学习Git,教程配图200张,其中包括包括Git基本命令、命令原理、Git底层命令、分支、分支的原理、Git代码冲突原理/解决、tag标签、Git存储状态、分支合并原理、典型合并、快进合并、同轴开发、非同轴开发、…...

【高等数学】导数与微分
文章目录 1、导数的概念1.1、引例1.1.1、变速直线运动瞬时速度1.1.2、曲线的切线 1.2、导数的定义1.3、证明常用导数1.4、导数的几何意义1.5、可导与连续的关系 2、函数的求导法则2.1、函数的和、差、积、商的求导法则2.2、反函数的求导法则2.3、复合函数的求导法则2.4、基本初…...

springboot之quartz动态可控定时任务
Quartz Quartz是一个开源的任务调度框架,可以用来实现定时任务的调度,如定时发送邮件、定时备份数据等。Quartz具有很高的可靠性和灵活性,支持集群部署和分布式调度,并且提供了丰富的API和插件,可以轻松实现复杂的调度…...

什么是CSS的外边距重叠?
区块的上下外边距有时会合并(折叠)为单个边距,其大小为两个边距中的最大值(或如果它们相等,则仅为其中一个),这种行为称为外边距折叠。注意:有设定浮动和绝对定位的元素不会发生外边…...

设计模式之抽象工厂模式
前言 工厂模式一般指的是简单工厂模式、工厂方法模式、抽象工厂模式,这是三种工厂模式的最后一篇,其他两种的文章链接如下: 设计模式之简单工厂模式-CSDN博客 设计模式之工厂方法模式-CSDN博客 建议三种模式放在一起对比学习,…...

Compose预处理组件大比拼:性能、应用场景和可视化对比总结
在机器学习的世界里,预处理组件就像是厨师的烹饪工具。选择合适的工具不仅可以让整个烹饪过程更加顺畅,还能确保最终的菜肴更加美味。 本文将深入探讨四种“烹饪工具”:TransformedTargetRegressor、make_column_transformer、make_column_selector和ColumnTransformer。通…...

【小米】Linux 实习生
下午不准备去图书馆自习来着,中午就狠狠地多睡了一个小时,三点起床靠在椅子上剥柚子,太爽了,这秋天的下午。“邮件:小米公司邀请你预约面试时间”.......... 我擦,投了一个月了,认真准备的时候…...

python一点通:coroutine (协程)是什么和重要知识点?
协程已经成为Python用于编写并发和异步代码的重要工具之一。在这篇博客文章中,我们将深入探讨协程是什么,它们的优点,以及它们与传统的线程和进程有何不同。 什么是协程? 协程是用于合作式多任务处理的子程序(或函数…...

QCC51XX-QCC30XX系列开发教程(实战篇) 之 12.1-空间音频相关模块的概述
查看全部教程开发请点击:全网最全-QCC51xx-QCC30xx(TWS)系列从入门到精通开发教程汇总(持续更新中) ==================================================================== 版权归作者所有,未经允许,请勿转载。 ==========================================...

Servlet的生命周期
2023.10.18 WEB容器创建的Servlet对象,这些Servlet对象都会被放到一个集合当中(HashMap),这个集合当中存储了Servlet对象和请求路径之间的关系 。只有放到这个HashMap集合中的Servlet才能够被WEB容器管理,自己new的Ser…...

2.4 如何在FlinkSQL使用DataGen(数据生成器)
1、DataGen SQL 连接器 FLinkSQL中可以使用内置的DataGen SQL 连接器来生成测试数据 官网链接:DataGen SQL 连接器 2、随机数数据生成器 随机数数据生成器支持随机生成 char、varchar、binary、varbinary、string 类型的数据 它是一个无界流的数据生成器 -- TO…...

Gin + Ant Design Pro JWT认证
文章目录 一:介绍二:Gin JWT 后台1. Claims 定义2. 创建和解析Token3. Gin中间件编写4. 辅助函数 三:Ant Design Pro JWT认证四:Gin中间件和使用示范 一:介绍 JWT现在比较流行的认证方式,微服务中使用特别…...

canvas实现图片标注,绘制区域
使用canvas绘制通过多边形标注区域 AI视频项目中需要分析图片,需要前台绘制区域,后端获取坐标然后识别图像,通过canvas 获取点然后连线绘图 HEML代码段 <div class"areaDrawing"><img src"/assets/images/snapPhotos…...

SELECT COUNT(*) 会造成全表扫描吗?
前言 SELECT COUNT(*)会不会导致全表扫描引起慢查询呢? SELECT COUNT(*) FROM SomeTable 网上有一种说法,针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能…...

python考前复习(90题)
文章目录 1.Python特性的是( )。 A. 面向对象 B. 高可移植性 C. 开源、免费 2.临时改变Python语言安装源应当使用的选项是 –index-url 3.Python脚本文件的扩展名为( ) .py 4.安装Python语言的软件包使用的命令是( ) pip install 5 . (单选题)以下哪项是…...