Vue3 + Nodejs 实战 ,文件上传项目--大文件分片上传+断点续传

news2024/11/19 8:26:52

 

目录

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) // 文件名,采用hash
  let 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 = null
    fileList.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].path
      const 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.body
  console.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 } = data
  const 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 = 0
      let 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].path
      const 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个

这时候重新选择该视频重新上传

这时候进度条很快就能到对应的进度

后端也没有上传多余的文件,直接接着传输,速度很快就传输完并且合并成了视频

到这基本的分片传输 + 断点续传就实现了,可能还存在者一些小问题,这个大家再项目中根据需求来做相应的改变。

具体的详细代码请大家到仓库下载,或者可以去我的主页资源中下载源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1112538.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[牛客]计算机网络习题笔记_1019

1、物理层&#xff1a;以太网 调制解调器 电力线通信(PLC) SONET/SDH G.709 光导纤维 同轴电缆 双绞线等。 2、数据链路层&#xff08;网络接口层包括物理层和数据链路层&#xff09;&#xff1a;Wi-Fi(IEEE 802.11) WiMAX(IEEE 802.16) ATM DTM 令牌环 以太网 FDD…

高校教务系统登录页面JS分析——华东交通大学

高校教务系统密码加密逻辑及JS逆向 本文将介绍高校教务系统的密码加密逻辑以及使用JavaScript进行逆向分析的过程。通过本文&#xff0c;你将了解到密码加密的基本概念、常用加密算法以及如何通过逆向分析来破解密码。 本文仅供交流学习&#xff0c;勿用于非法用途。 一、密码加…

android studio打开flutter项目报红

一、android studio打开flutter项目报红&#xff0c;如下图&#xff1a; 二、解决方法&#xff1a; 2.1 在这个build.gradle添加以下代码&#xff0c;如图&#xff1a; 2.2 在build.gradle最顶部添加如下代码&#xff1a; def localProperties new Properties() def localPr…

水经注地图服务 5.0.1-rc 版发布

《水经注地图服务》&#xff08;WeServer&#xff09;是一款可快速发布全国乃至全球海量卫星影像的地图发布服务产品。 它可以轻松发布260TB级海量卫星影像&#xff0c;从而使“在内网建立一个离线版的地球”不只是一个梦想&#xff01; ​01 新版发布 水经注地图服务 5.0…

NodeMCU ESP8266 读取按键外部输入信号详解(图文并茂)

NodeMCU ESP8266 读取按键外部输入信号教程&#xff08;图文并茂&#xff09; 文章目录 NodeMCU ESP8266 读取按键外部输入信号教程&#xff08;图文并茂&#xff09;前言按键输入常用接口pinModedigitalRead 示例代码结论 前言 ESP8266如何检测外部信号的输入&#xff0c;通常…

10kV-35kV交联电缆油杯终端

武汉凯迪正大油杯产品简介 KDZD-10 /KDZD-35 油杯终端是我公司在总结了大量的现场经验的基础上&#xff0c;自行开发、设计的一种 10~35kV 以下交联电缆和工频耐压试验的简易试验终端&#xff0c;该油杯操作简便&#xff0c;使用可靠。 目前电缆厂均拥有多条 XLPE 生产线&…

【Git】升级MacOS系统,git命令无法使用

终端执行git命令报错 xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun安装这个东东&#xff0c;&#xff1f;需要42小时 最终解决&#xff1a; 下载安装 https…

C语言进阶第七课-----------自定义类型的讲解(结构体枚举联合)

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

2023CRM排行:深度对比16款CRM

客户关系管理系统&#xff08;CRM&#xff09;作为数字化转型的重要载体&#xff0c;选择一个优秀的CRM系统将为企业未来健康增长保障。市场上CRM软件众多&#xff0c;但很难分清哪个适合自己&#xff0c;最近赶在公司选型&#xff0c;我对市场所有软件进行了一个调研&#xff…

postgresql(openGauss)模糊匹配参数

被pg系这个show要求精准匹配参数恶心的不轻。 原理是用.psqlrc&#xff08;openGauss用.gsqlrc&#xff09;文件set一个select常量进去&#xff0c;需要用&#xff1a;调用这个常量。理论上也可以增强其他的各种功能。 我在openGauss做的一个例子 .gsqlrc&#xff08;.psqlrc…

容灾备份——容灾系统介绍

目录 基本概述 容灾关键技术 容灾系统的级别 容灾主要技术 基本概述 容灾与备份的区别 容灾备份——备份技术系统架构与备份网络方案-CSDN博客https://blog.csdn.net/m0_49864110/article/details/123969802?ops_request_misc%257B%2522request%255Fid%2522%253A%252216…

【Java 进阶篇】JavaScript 表单验证详解

JavaScript 表单验证是网页开发中不可或缺的一部分。它允许您确保用户在提交表单数据之前输入了有效的信息。无论您是一个初学者还是一个有经验的开发人员&#xff0c;本文将为您详细介绍如何使用 JavaScript 来进行表单验证。我们将从基础知识开始&#xff0c;逐步深入&#x…

ubuntu20.04运用startup application开机自启动python程序

运用startup application开机自启动python程序。在终端中输入gnome-session-properties,如果显示没有则先进行安装&#xff0c;sudo apt-get update 和sudo apt install StartupApplications(根据显示提示安装)。在显示程序中搜索startup&#xff0c;打开应用程序。 在程序目录…

LeetCode 1595. 连通两组点的最小成本【记忆化搜索,状压DP】2537

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

环形链表的约瑟夫问题

前言&#xff1a; 据说著名犹太历史学家Josephus有过如下故事&#xff1a; 在罗马人占领乔塔帕特后&#xff0c;39个犹太人和Josephus及他的朋友躲进一个洞里&#xff0c;39个犹太人决定宁愿死也不要被敌人抓到&#xff0c;于是决定了一个自杀方式&#xff0c;41个人排成一个…

24、Flink 的table api与sql之Catalogs(java api操作分区与函数、表)-4

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

Unity3D 关于过大的UI帧动画如何处理详解

Unity3D是一款流行的游戏开发引擎&#xff0c;它可以用来创建各种类型的游戏&#xff0c;包括2D和3D游戏。在游戏中&#xff0c;UI帧动画是一个常见的元素&#xff0c;它可以增加游戏的交互性和视觉效果。然而&#xff0c;当UI帧动画过大时&#xff0c;可能会导致游戏的性能下降…

linux安装新版本git2、配置github-ssh。(centos、aws)

一、安装Git 1、yum默认版本git #1.安装git sudo yum install git -y #2.确认Git已经安装成功 git --version如果要安装较新版本&#xff0c;可以安装一个repo &#xff0c;但是我这第一次尝试失败了&#xff0c;执行完提示找不到git2u&#xff0c;ius repo也连不上。而且每次…

02HTML功能元素

1.功能元素 1.1.列表标签 ​ 列表标签的作用: 给一堆数据添加列表语义, 也就是告诉搜索引擎告诉浏览器这一堆数据是一个整体 - HTML中列表标签的分类 ​ 无序列表(最多)(unordered list) ​ 有序列表(最少)(ordered list) ​ 定义列表(其次)(definition list) 1.1.1.无序列…

解析Apache Kafka中的事务机制

这篇博客文章并不是关于使用事务细节的教程&#xff0c;我们也不会深入讨论设计细节。相反&#xff0c;我们将在适当的地方链接到JavaDocs或设计文档&#xff0c;以供希望深入研究的读者使用。 为什么交易? 我们在Kafka中设计的事务主要用于那些显示“读-进程-写”模式的应用…