前端必学 - 大文件上传如何实现

article/2025/8/18 22:17:38

前端必学 - 大文件上传如何实现

  • 写在前面
  • 问题分析
  • 开始操作
    • 一、文件如何切片
    • 二、得到原文件的hash值
    • 三、文件上传
    • 四、文件合并
  • 技术点总结【重要】
    • 一、上传文件?
    • 二、显示进度
    • 三、暂停上传
    • 四、Hash有优化空间吗?
    • 五、限制请求个数
    • 六、拥塞控制,动态计算文件切片大小
  • 演示&源码

写在前面

1、正常的向后端发送请求,常见的 getpost 大家都很熟悉,是没有任何问题的;我们也可以用 post 或者表单请求发送 file文件 到后端。 但是大文件的上传是一个特殊的情况: 大文件上传最主要的问题就在于:在一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。

  • 首先是上传过程时间比较久(要传输更多的报文,丢包重传的概率也更大),在这个过程中不能做其他操作,用户不能刷新页面,只能耐心等待请求完成。
  • 常见的软件应用中,前端/后端都会对一个请求的时间进行限制,那么大文件的上传就会很容易超时,导致上传失败。
  • 上传失败只能从头再来,你能接受吗?

2、面试/实际工作中,这也是一个常见的问题;所以,我们今天来彻底搞懂它。

源代码:https://github.com/Neveryu/bigfile-upload

问题分析

如果我们将这个文件拆分,将一次性上传大文件拆分成多个上传小文件的请求,因为请求是可以并发的,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样不就可以解决大文件上传的问题了!

【明确目标】大文件上传需要实现下面几个需求:

  • 支持拆分上传请求(即文件切片)
  • 支持断点续传
  • 支持显示上传进度和暂停上传

开始操作

一、文件如何切片

用户选择了一个大文件后,我们该如何处理它?

JavaScript 中,文件 File 对象是 Blob 对象的子类,Blob 对象包含一个重要的方法 slice,通过这个方法,我们就可以对二进制文件进行拆分。

 // 生成文件切片
function createFileChunk(file, size = SIZE) {const fileChunkList = []let cur = 0while (cur < file.size) {fileChunkList.push({file: file.slice(cur, cur + size),})cur += size}return fileChunkList
}

将文件拆分成 size 大小(可以是100k、500k、1M…)的分块,得到一个 file 的数组 fileChunkList,然后每次请求只需要上传这一个部分的分块即可。服务器接收到这些切片后,再将他们拼接起来就可以了。

二、得到原文件的hash值

拿到原文件的 hash 值是关键的一步,同一个文件就算改文件名,hash 值也不会变,就可以避免文件改名后重复上传的问题。

这里,我们使用 spark-md5.min.js 来根据文件的二进制内容计算文件的 hash

说明:考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互。

由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts 函数用于导入外部脚本,通过它导入 spark-md5。

计算 hash 代码如下:

// public/hash.js
self.onmessage = e => {const { fileChunkList } = e.dataconst spark = new self.SparkMD5.ArrayBuffer()let percentage = 0let count = 0const loadNext = index => {const reader = new FileReader()reader.readAsArrayBuffer(fileChunkList[index].file)reader.onload = e => {count++spark.append(e.target.result)if (count === fileChunkList.length) {self.postMessage({percentage: 100,hash: spark.end()})self.close()} else {percentage += 100 / fileChunkList.lengthself.postMessage({percentage})loadNext(count)}}}loadNext(count)
}

我们传入切片后的 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程。

【重要说明】spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档 spark-md5。

三、文件上传

1)验证文件是否已经在服务端存在,如果存在,那就不用上传了,相当于秒传成功。

/*** 返回值说明* shouldUpload:标识这个文件是否还需要上传* uploadedList: 服务端存在该文件的切片List*/
const { shouldUpload, uploadedList } = await verifyUpload(container.file.name,container.hash
)

如果 shouldUploadfalse,则表明这个文件不需要上传,提示:秒传成功。

2)然后上传除了 uploadedList 之外的文件切片。

 /**
* 上传切片,同时过滤已上传的切片
* uploadedList:已经上传了的切片,这次不用上传了
*/
async function uploadChunks(uploadedList = []) {console.log(uploadedList, 'uploadedList')const requestList = data.value.filter(({ hash }) => !uploadedList.includes(hash)).map(({ chunk, hash, index }) => {const formData = new FormData()// 切片文件formData.append('chunk', chunk)// 切片文件hashformData.append('hash', hash)// 大文件的文件名formData.append('filename', container.file.name)// 大文件hashformData.append('fileHash', container.hash)return { formData, index }}).map(async ({ formData, index }) =>request({url: 'http://localhost:9999',data: formData,onProgress: createProgressHandler(index, data.value[index]),requestList: requestListArr.value,}))// 并发切片await Promise.all(requestList)// 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时// 切片并发上传完以后,发个请求告诉后端:合并切片if (uploadedList.length + requestList.length === data.value.length) {// ok,都上传完了,请求合并文件mergeRequest()}
}

四、文件合并

文件合并方案有这么几种。

1、前端发送切片完成后,发送一个合并请求,后端收到请求后,将之前上传的切片文件合并。
2、后台记录切片文件上传数据,当后台检测到切片上传完成后,自动完成合并。
3、创建一个和源文件大小相同的文件,根据切片文件的起止位置直接将切片写入对应位置。

我们这里采用的是第一种方案。

下面以用 node.js 的实现为例:

/*** 合并文件夹中的切片,生成一个完整的文件* @Author   Author* @DateTime 2021-12-30T17:41:19+0800* @param    {[string]}                 filePath [完整的文件路径(最终文件切片合并为一个完整的文件)]* @param    {[type]}                 fileHash [大文件的文件名]* @param    {[type]}                 size     [单个切片的大小]* @return   {[type]}                          [description]*/
const mergeFileChunk = async (filePath, fileHash, size) => {// 所有的文件切片放在以“大文件的文件hash命名文件夹”中const chunkDir = path.resolve(UPLOAD_DIR, fileHash)const chunkPaths = await fse.readdir(chunkDir)// 根据切片下标进行排序// 否则直接读取目录的获得的顺序可能会错乱chunkPaths.sort((a, b) => {return a.split('-')[1] - b.split('-')[1]})await Promise.all(chunkPaths.map((chunkPath, index) => {return pipeStream(path.resolve(chunkDir, chunkPath),/*** 创建写入的目标文件的流,并指定位置,* 目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,* 所以这里还需要让前端在请求的时候多提供一个 size 参数。* 其实也可以等上一个切片合并完后再合并下个切片,这样就不需要指定位置,* 但传输速度会降低,所以使用了并发合并的手段,*/fse.createWriteStream(filePath, {start: index * size,end: (index + 1) * size}))}))// 文件合并后删除保存切片的目录fse.rmdirSync(chunkDir)
}

服务端根据文件标识,分片顺序进行合并,合并完以后删除分片文件。

技术点总结【重要】

一、上传文件?

我们都知道如果要上传一个文件,需要把 form 标签的 enctype 设置为 multipart/form-data,同时method 必须为 post 方法。(这是最原始的方式)

那么 multipart/form-data 表示什么呢?

multipart 互联网上的混合资源,就是资源由多种元素组成,form-data 表示可以使用 HTML Forms 和 POST 方法上传文件,具体的定义可以参考 RFC 7578。

但是现在,我们很少使用这种 form 的方式了,我们都是直接使用 XMLHttpRequest 来发送 Ajax 请求。

最开始 XMLHttpRequest 是不支持传输二进制文件的。文件只能使用表单的方式上传,我们需要写一个 Form,然后将 enctype 设置为 multipart/form-data

后来 XMLHttpRequest 升级为 Level 2 之后,新增了 FormData 对象,用于模拟表单数据,并且支持发送和接收二进制数据。我们目前使用的文件上传基本都是基于 XMLHttpRequest Level 2

xhr.send(data)data 参数的数据类型会影响请求头部 content-type 的值。我们上传文件,data 的类型是 FormData,此时 content-type 默认值为 multipart/form-data在上传文件场景下,不必设置 content-type 的值,浏览器会根据文件类型自动配置

二、显示进度

我们可以通过 onprogress 事件来实时显示进度,默认情况下这个事件每 50ms 触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的 onprogress 事件:上传触发的是 xhr.upload 对象的 onprogress 事件,下载触发的是 xhr 对象的 onprogress 事件。

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;function updateProgress(event) {if (event.lengthComputable) {var completedPercent = event.loaded / event.total;}
}

PS 特别提醒:xhr.upload.onprogress 要写在 xhr.send 方法前面。

三、暂停上传

一个请求能被取消的前提是,我们需要将未收到响应的请求保存在一个列表中,然后依次调用每个 xhr 对象的 abort 方法。调用这个方法后,xhr 对象会停止触发事件,将请求的 status 置为 0,并且无法访问任何与响应有关的属性。

/*** 暂停*/
function handlePause() {requestListArr.value.forEach((xhr) => xhr?.abort())requestListArr.value = []
}

从后端的角度看,一个上传请求被取消,意味着当前浏览器不会再向后端传输数据流,后端此时会报错,如下,错误信息也很清楚,就是文件还没到末尾就被客户端中断。当前文件切片写入失败。

四、Hash有优化空间吗?

计算 hash 耗时的问题,不仅可以通过 web-workder,还可以参考 ReactFFiber 架构,通过 requestIdleCallback 来利用浏览器的空闲时间计算,也不会卡死主线程;

如果觉得文件计算全量 Hash 比较慢的话,还有一种方式就是计算抽样 Hash,减少计算的字节数可以大幅度减少耗时;

在前文的代码中,我们是将大文件切片后,全量传入 spark-md5.min.js 中来根据文件的二进制内容计算文件的 hash 的。

那么,举个例子,我们可以这样优化: 文件切片以后,取第一个和最后一个切片全部内容,其他切片的取 首中尾 三个地方各2各字节来计算 hash。这样来计算文件 hash 会快很多。

五、限制请求个数

解决了大文件计算 hash 的时间优化问题;下一个问题是:如果一个大文件切了成百上千来个切片,一次发几百个 http 请求,容易把浏览器搞崩溃。那么就需要控制并发,也就是限制请求个数

思路就是我们把异步请求放在一个队列里,比如并发数是4,就先同时发起4个请求,然后有请求结束了,再发起下一个请求即可。

我们通过并发数 max 来管理并发数,发起一个请求 max--,结束一个请求 max++ 即可。

【预留】

六、拥塞控制,动态计算文件切片大小

【预留】

演示&源码

源代码:https://github.com/Neveryu/bigfile-upload

在这里插入图片描述

源代码:https://github.com/Neveryu/bigfile-upload

—————————— 【正文完】——————————

前端学习交流群,想进来面基的,可以加群: 685486827,832485817;
Vue学习交流 React学习交流

写在最后: 约定优于配置 —— 软件开发的简约原则

——————————【完】——————————

我的:
个人网站: https://neveryu.github.io/neveryu/
Github: https://github.com/Neveryu
新浪微博: https://weibo.com/Neveryu
微信: miracle421354532

更多学习资源请关注我的新浪微博…好吗


http://chatgpt.dhexx.cn/article/y7K65Xo4.shtml

相关文章

Linux如何快速生成大文件

微信搜索&#xff1a;“二十同学” 公众号&#xff0c;欢迎关注一条不一样的成长之路 dd命令 dd if/dev/zero offile bs1M count20000 会生成一个20G的file 文件&#xff0c;文件内容为全0&#xff08;因从/dev/zero中读取&#xff0c;/dev/zero为0源&#xff09;。 此命令可…

java 处理大文件

目的&#xff1a; 前几天在开发过程中遇到一个需求: 读取一个大约5G的csv文件内容&#xff0c;将其转化为对象然后存储到redis中, 想着直接开大内存直接load 进入到内存中就行了&#xff0c;结果可想而知,5G的文件 &#xff0c;Xmx 开到10G都没有解决&#xff0c;直接out of Me…

5、Linux:如何将大文件切割成多份小文件

最近&#xff0c;在做数据文件的导入操作时&#xff0c;发现有些文本文件太大了&#xff0c;需要将这样的大文件切分成多个小文件进行操作。那么&#xff0c;Linux 中如何将大文件切割成许多的小文件呢&#xff1f;在此记录一下。 Linux 提供了 split 命令可以轻松实现大文件的…

大文件传输有哪些方式可用?大文件传输有哪些方式?

大文件传输有哪些方式可用&#xff1f;大文件传输有哪些方式&#xff1f;互联网时代&#xff0c;速度决定效率。在企业生产过程中需要进行信息数据交换、搬运。这时就需要进行大文件传输。方方面面的行业都要涉及到大文件传输。例如影视行业需要每天进行视频素材的传输&#xf…

简道云-第5章-流程

title: 简道云-第5章-流程 date: 2022-06-13 22:21:29 tags: 简道云 categories: 简道云 简道云-第5章-流程 背景介绍 简道云三个基本项目表单、流程以及仪表。关于它们的介绍可以参照官方文档表单 vs 流程表单 vs 仪表盘。 「流程表单」&#xff1a;填报数据&#xff0c;并带…

阿里云【达摩院特别版·趣味视觉AI训练营】笔记2

阿里云【趣味视觉AI训练营】笔记2 一、笔记说明二、正文2.1 人体分割实验2.2 图像人脸融合实验 三、转载说明 一、笔记说明 本博客专栏《阿里云【达摩院特别版趣味视觉AI训练营】》的所有文章均为趣味视觉AI训练营的学习笔记&#xff0c;当前【达摩院特别版趣味视觉AI训练营】…

笔记本简单使用eNSP的云连接外网

文章目录 前言一、连接拓扑图二、配置cloud 三、配置pc测试是否能连接外网 前言 很多时候ping不通的原因不是网卡问题&#xff0c;而是配置没有设置好 一、连接拓扑图 二、配置cloud 绑定信息为UDP然后点击增加 绑定信息 笔记本电脑可以选择WiFi-ip&#xff0c;有本地连接可以…

头歌-信息安全技术-用Python实现自己的区块链、支持以太坊的云笔记服务器端开发、编写并测试用于保存云笔记的智能合约、支持以太坊的云笔记小程序开发基础

头歌-信息安全技术-用Python实现自己的区块链、支持以太坊的云笔记服务器端开发、编写并测试用于保存云笔记的智能合约、支持以太坊的云笔记小程序开发基础 一、用Python实现自己的区块链1、任务描述2、评测步骤(1)打开终端&#xff0c;输入两行代码即可评测通过 二、支持以太坊…

华为云HCS解决方案笔记HUAWEI CLOUD Stack【面试篇】

目录 HCS方案 一、定义 1、特点 2、优点 二、云服务 1、云管理 2、存储服务 3、网络服务 4、计算服务 5、安全服务 6、灾备服务 7、容器服务 三、应用场景 四、HCS功能层 五、OpenStack网络平面规划 六、ManageOne运维面 1、首页 2、集中监控 3、资源拓扑 …

关于玄武集团MOS云平台的使用笔记

对于该平台感兴趣的可以自己下载开发文档看一下&#xff0c;附上地址: https://download.csdn.net/download/qq_39380192/11182359 1、根据开发手册&#xff0c;MOS云平台给用户提供了关于各种通信服务的接口&#xff0c;用户可以通过调用相关的接口来实现一下几点功能&#x…

巧用git commit搭建云笔记+历史记录本

一、整理笔记的必要性 长期学习过程中&#xff0c;我发现人脑并不擅长记忆&#xff0c;它更擅长思考问题。程序员每天都要学习很多知识&#xff0c;学得快&#xff0c;忘得快很正常。很多东西并不需要记住&#xff0c;况且知识那么多&#xff0c;怎么可能全部记住&#xff1f;…

Aliyun 学习笔记(二)阿里云物联网平台介绍

文章目录 1 阿里云物联网平台1.1 设备接入1.2 设备管理1.3 安全能力1.4 规则引擎 1 阿里云物联网平台 根据阿里云物联网平台文档可以了解到所有有关阿里云物联网平台的介绍。 阿里云物联网平台为设备提供安全可靠的连接通信能力&#xff0c;向下连接海量设备&#xff0c;支撑…

《没道云笔记》开发手记

基本配置 Client&#xff1a;Android Servlet&#xff1a;SAE&#xff08;PHPMySQLStorage&#xff09; Period&#xff1a;2 weeks 项目分析 1.Model: Article.class{int id;String username;String title;String time;String content;} Bean.calss{int[] ids;String u…

《物联网 - 机智云开发笔记》第2章 设备驱动开发

开发板&#xff1a;GoKit3开发板&#xff08;STM32F103&#xff09; 在上一章节&#xff0c;笔者带领大家已经将机智云平台玩起来&#xff0c;本节内容讲带领大家经进一步开发。 在开始讲解之前&#xff0c;有必要先了解的机智云的平台架构。 从上面的架构图可以看到&#xf…

云笔记的使用感受和选择

市场上有很多文章针对云笔记的选择&#xff0c;但经过下载发现可能存在很多虚假广告【求生欲&#xff1a;其实可能是个人使用感受不佳仅表示个人观点】。 为什么选择云笔记 个人比较喜欢(❤ ω ❤)记录学习笔记和生活中的东西。之前选择有道云笔记&#xff0c;但因为最近打开…

基于分布式的云笔记实现(参考某道云笔记)

注&#xff1a; 1&#xff09;云笔记代码可在github上下载&#xff0c;如果对您有用&#xff0c;记得star一下。 2&#xff09;依赖jar包可在以下地址下载jar包&#xff0c;密码&#xff1a;yvkj&#xff0c;放到web/lib下即可 3&#xff09;hdfs配置参考网址 4&#xff09…

高软作业1:云笔记软件调研

写在前面&#xff1a; 选择云笔记作为这次调研对象&#xff0c;是因为看到一位同学作业里关于iOS场景下面的笔记软件对比。这一下子让我想起自己入坑过的各款云笔记应用&#xff0c;他们基本上都拥有云端存储和多端同步的功能&#xff0c;但同时又都存在着各自的优缺点。本来一…

华为云学习笔记(二)

物联网发展简史与概述 物联网大事件&#xff1a;NB-lot标准演进 NB-lot&#xff1a; 窄带物联网&#xff08;Narrow Band Internet of Things, NB-IoT&#xff09;成为万物互联网络的一个重要分支。NB-IoT构建于蜂窝网络&#xff0c;只消耗大约180kHz的带宽&#xff0c;可直接…

阿里云笔记2.0

【达摩院特别版趣味视觉AI训练营】 智能视觉开放平台> 目的&#xff1a;致力于更专业、全面、易用的视觉AI能力 图片4通道和3通道的对比成效 人体分割技术&#xff0c;适应复杂背景 即使人物处于复杂背景环境&#xff0c;依然可以将人体准确地从背景中分割出来。 阿里云…

SG90舵机的驱动

很多资料说SG90舵机是靠PWM控制的&#xff0c;其实不是&#xff0c;亲测该舵机是靠高电平的时间控制的&#xff0c;只是对周期有要求&#xff0c;需要是20ms以内&#xff0c;没错20ms的周期时间不是固定周期&#xff0c;而是最大周期。我尝试了3-20ms都可以控制。 高电平的时间…