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博客 建议三种模式放在一起对比学习,…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...
6.9-QT模拟计算器
源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…...
raid存储技术
1. 存储技术概念 数据存储架构是对数据存储方式、存储设备及相关组件的组织和规划,涵盖存储系统的布局、数据存储策略等,它明确数据如何存储、管理与访问,为数据的安全、高效使用提供支撑。 由计算机中一组存储设备、控制部件和管理信息调度的…...
Spring Boot 与 Kafka 的深度集成实践(二)
3. 生产者实现 3.1 生产者配置 在 Spring Boot 项目中,配置 Kafka 生产者主要是配置生产者工厂(ProducerFactory)和 KafkaTemplate 。生产者工厂负责创建 Kafka 生产者实例,而 KafkaTemplate 则是用于发送消息的核心组件&#x…...
