WEB前端开发中如何实现大文件上传?

大文件上传是个非常普遍的场景,在面试中也会经常被问到,大文件上传的实现思路和流程。在日常开发中,无论是云存储、视频分享平台还是企业级应用,大文件上传都是用户与服务器之间交互的重要环节。随着现代网络应用的日益复杂化,大文件上传已经成为前端开发中不可或缺的一部分。

然而,在实现大文件上传时,我们通常会面临以下几个挑战:

  1. 上传超时一般前端请求都会限制最大请求时长,比如axios设置timeout,或者是 nginx(或其它代理/网关) 限制了最大请求时长。

  2. 服务器压力:大文件上传会给服务器带来较大的压力,甚至可能导致服务器崩溃。

  3. 文件大小超限:一般后端都会对上传文件的大小做限制,比如nginx和server都会限制。

  4. 用户体验:上传过程中用户需要等待较长时间,用户体验差。

  5. 网络波动各种网络原因导致上传失败,比如网络不稳定可能导致上传过程中断,且失败之后需要从头开始。

对于前三点,虽说可以通过一定的配置来解决,但有时候也相当麻烦,或者服务器就规定不允许上传大型文件,需要兼顾实际场景。上传慢的话倒是无伤大雅,忍一忍是可以接受的,只是体验不好,但是失败后在重头开始上传,在网络环境差的时候简直就是灾难。为了应对以上挑战,我们就需要用到切片上传、断点续传等技术手段。

二、实现思路分析

整体流程图如下:

思路如下:

  1. 每个文件要有自己唯一的标识,因此在进行分片上传前,需要对整个文件进行MD5加密,生成MD5码,在后面上传文件每次调用接口时以formData格式上传给后端。可以使用spark-md5 计算文件的内容hash,以此来确定文件的唯一性将文件hash发送到服务端进行查询。以此来确定该文件在服务端的存储情况,这里可以分为三种:未上传、已上传、上传部分。

  2. 根据服务端返回的状态执行不同的上传策略。已上传:执行秒传策略,即快速上传,实际上没有对该文件进行上传,因为服务端已经有这份文件了。未上传、上传部分:执行计算待上传分块的策略并发上传还未上传的文件分块。当传完最后一个文件分块时,向服务端发送合并的指令,即完成整个大文件的分块合并,实现在服务端的存储。

上传过程:

  1. 分割文件:将要上传的文件切割成多个小文件片段。主要使用JavaScript的File API中的slice方法来实现。

  2. 上传文件分片:使用XMLHttpRequest或者Fetch API将分片信息以formData格式,并携带相关信息,如文件名、文件ID、当前片段序号等参数传给分片接口。

  3. 后端接收并保存文件片段:后端接收到每个文件片段后,将其保存在临时位置,并记录文件片段的序号、文件ID和文件MD5 hash值等信息。

  4. 续传处理:如果上传过程中断,下次继续上传时,通过查询后端已保存的文件片段信息,得知需要上传的文件片段,从断点处继续上传剩余的文件片段。

  5. 合并文件:当所有文件片段都上传完成后,后端根据文件ID将所有片段合并成完整的文件。

三、切片上传

切片上传原理:通过使用JavaScript的File API中的slice方法将大文件分割成多个小片段(chunk),然后逐个上传每个片段,在上传完切片后,前端通知后台再将文件片段拼接为一个完整的文件。

这样做的优点是可以并行多个请求一起上传文件,提高上传效率,并且在上传过程中如果某个片段因为某些原因上传失败,也不会影响其它文件切片,只需要重新上传该失败片段即可,不必重新上传整个文件。

实现思路:

在JavaScript中,文件File对象是Blob对象的子类,Blob对象包含了slice方法,通过这个方法,可以对二进制文件进行拆分。循环发送多个上传请求,然后返回结果后计数,当计数达到file片段长度后终止上传。

<input type="file" name="file" id="file" />
const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);uploadChunk(chunk);currentPosition += chunkSize;}
})function uploadChunk(chunk) {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 根据项目实际情况axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {// 上传成功console.log('分片上传成功', res)}).catch(error => {// 上传失败console.log('分片上传失败', error)})
}

四、并发上传

并发上传相对要优雅一下,将文件分割成小片段后,使用Promise.all()把所有请求都放到一个Promise.all里,它会自动判断所有请求都完成然后触发 resolve 方法。并发上传可以同时上传多个片段而不是依次上传,进一步提高效率。

实现思路:

1、使用slice方法对二进制文件进行拆分,并把拆分的片段放到chunkList里面。

2、使用map将chunkList里面的每个chunk映射到一个Promise上传方法。

3、把所有请求都放到一个Promise.all里,它会自动判断所有请求都完成然后触发 resolve 方法,上传成功后通知后端合并分片文件。

代码实现如下:

const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;// 存储文件的分片let chunkList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);chunkList.push(chunk);currentPosition += chunkSize;}uploadChunk(chunkList, file.name)
})function uploadChunk(chunkList, fileName) {const uploadPromiseList = chunkList.map((chunk, index) => {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 根据项目实际情况return axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})})Promise.all(uploadPromiseList).then(res => {// 上传成功并通知后端合并分片文件axios.post('/api/oss/file/merge', {message: fileName},{headers: { 'Content-Type': 'application/json' },timeout: 600000,}).then(data => {console.log('文件合并成功', data)})}).catch(error => {// 上传错误console.log('上传失败', error)})
}

五、断点续传之1

断点续传允许在网络中断或其它原因导致上传失败时,从上次上传中断的位置继续上传,而不是重新从头上传整个文件。

实现断点续传需要后端配合记录上传的进度,并且在前端重新上传时,需要先查询已上传的进度,让后从断点处继续上传。

const eleFile = document.getElementById('file');
eleFile.addEventListener('change', (event) => {const file = event.target.files[0];// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * 1;// 当前已执行分片数位置let currentPosition = 0;// 存储文件的分片let chunkList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;while(currentPosition < file.size) {const chunk = blobSlice.call(file, currentPosition, currentPosition + chunkSize);chunkList.push(chunk);currentPosition += chunkSize;}axios.post('/api/upload/file/history',{fileName: file.name},{headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {const historyChunks = res.uploadedChunks;const remainChunks = chunkList.filter((item, index) => !historyChunks.includes(index));// 并发上传剩余分片uploadChunk(remainChunks, file.name)})
})function uploadChunk(chunkList, fileName) {const uploadPromiseList = chunkList.map((chunk, index) => {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', chunk);// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 根据项目实际情况return axios.post('/api/oss/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})})Promise.all(uploadPromiseList).then(res => {// 剩余分片上传成功并通知后端合并分片文件axios.post('/api/oss/file/merge', {message: fileName},{headers: { 'Content-Type': 'application/json' },timeout: 600000,}).then(data => {console.log('文件合并成功', data)})}).catch(error => {// 上传错误console.log('上传失败', error)})
}

以上是一个简易版的断点续传实现流程代码,但在实际场景应用中我们还需要更严谨的处理来实现断点续传功能。不如,上传文件前通常需要生成文件的唯一标识,比如文件名与文件大小的组合、文件的hash值或者文件hash值与文件大小的组合来支持断点续传的逻辑。请继续看下面的代码实现!!!

六、断点续传之2

已上传的执行秒传策略,即快速上传,实际上没有对该文件进行上传,因为服务端已经有这份文件了。

秒传的关键在于计算文件的唯一性标识。文件的不同不是命名的差异,而是内容的差异,所以我们将整个文件的二进制码作为入参,计算 Hash 值,将其作为文件的唯一性标识。一般而言,这样做就够了,但是摘要算法是存在碰撞概率的,我们如果想要再严谨点的话,可以将文件大小也作为衡量指标,只有文件摘要和文件大小同时相等,才认为是相同的文件。

<input type="file" name="file" id="file" @change="changeFile" />

计算文件hash值可以使用spark-md5。

import SparkMD5 from 'spark-md5'

通过input的change事件获取要上传的文件。

function changeFile(event) {const file = event.target.files[0];handleUploadFile(file, 1)
}

接下来对文件进行分片和hash计算:

/*** @param {File} file 目标上传文件* @param {number} size 上传分块大小,单位Mb* @returns {filelist:ArrayBuffer,fileHash:string}*/
async function handleSliceFile(file, size = 1) {return new Promise((resolve, reject) => {// 上传分块大小,单位Mbconst chunkSize = 1024 * 1024 * size;// 分片数const totalChunkCount = file && Math.ceil(file.size / chunkSize);// 当前已执行分片数位置let currentChunkCount = 0;// 存储文件的分片let fileList = [];//初始化分片方法,兼容问题let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;// 文件读取对象const fileReader = new FileReader();// spark-md5 计算文件hash值SparkMD5对象const spark = new SparkMD5.ArrayBuffer();// 存储计算后的文件hash值let fileHash = "";// 错误fileReader.onerror = function () {reject('Error reading file');};fileReader.onload = (e) => {//当前读取的分块结果 ArrayBufferconst curChunk = e.target.result;//将当前分块追加到spark对象中spark.append(curChunk);currentChunkCount++;fileList.push(curChunk);//判断分块是否完成if (currentChunkCount >= totalChunkCount) {// 全部读取,获取文件hashfileHash = spark.end();resolve({ fileList, fileHash });} else {readNext();}};//读取下一个分块const readNext = () => {//计算分片的起始位置和终止位置const start = chunkSize * currentChunkCount;let end = start + chunkSize;if (end > file.size) {end = file.size}//读取文件,触发onLoadfileReader.readAsArrayBuffer(blobSlice.call(file, start, end))}readNext()})
}

文件上传,首选调用接口获取需要上传的文件index,返回的集合length等于0执行秒传,如果返回的集合length不等于0执行需要过滤得到需要上传的remainingChunks,使用map将remainingChunks里面的每个chunk映射为一个Promise上传方法,把所有请求都放到一个Promise.all里,上传成功后通知后端合并分片文件。

sync function handleUploadFile(file, chunkSize) {const { fileList, fileHash } = await handleSliceFile(file, chunkSize);// 存放切片let chunkList = fileList;// 显示上传的进度条let process = 0;// 获取文件上传状态const { data } = await axios.post('/api/upload/file/history', {fileHash,totalCount: chunkList.length,extname: file.name,})// 返回已经上传的const { needUploadChunks } = data;// 已上传,无待上传文件,秒传if (!needUploadChunks.length) {process = 100;return;} // 此处包含了未上传和上传部分的情况// 过滤剩余需要上传的分片序列const remainingChunks = chunkList.filter((item, index) => needUploadChunks.includes(index + 1));// 同步上传进度,断点续传情况下progress = ((chunkList.length - needUploadChunks.length) / chunkList.length) * 100;// 上传if (remainingChunks.length) {const uploadPromiseList = remainingChunks.map(async (chunk, index) => {const response = await uploadChunk(chunk, index + 1, fileHash);//更新进度progress += Math.ceil(100 / allChunkList.length);if (progress >= 100) progress = 100;return response;});Promise.all(uploadPromiseList).then(() => {// 清空已上传的切片chunkList = [];//发送请求,通知后端进行合并axios.post('/api/file/merge', {fileHash,extname: 'fileName.mp4'}, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,}).then(res => {console.log('合并完成', res)}).catch(error => {// 合并错误console.log('合并错误', error)})}).catch(error => {// 上传错误console.log('上传错误', error)})}
}

上传函数返回一个promise,参数为formData。

function uploadChunk(chunk, index, fileHash) {// 将分片信息以formData格式作为参数传给分片接口let formData = new FormData();formData.append('fileChunk', new Blob([chunk]));// 可以根据实际的需要添加其它参数,比如切片的索引formData.append('index', index);// 文件的标识hash值formData.append('fileHash', fileHash);// 根据项目实际情况return axios.post('/api/upload/file', formData, {headers: { 'Content-Type': 'multipart/form-data' },timeout: 600000,})
}

我们在 fileReader 里面使用了 readAsArrayBuffer 方法做转换并分割,因此传入的chunk的类型是ArrayBuffer,而formData中文件的类型应该是Blob,所以需要时用new Blob() 将每一个chunk转为Blob类型。

七、总结

断点续传的重点是文件的切片与合并,整个上传流程需要前后端配合好,细节较多。

注意事项:

  1. 计算整个文件的 MD5 值,当大文件比较大时会比较慢,耗时,更好地做法是将这部分任务放在 Web Worker 中执行。Web Worker 是 HTML5 标准的一部分,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。这样计算任务就不会影响到当前线程的渲染任务。可以和当前线程间使用 postMessage 的方式进行通讯。

  2. 可以根据文件切片的状态,发送上传请求,由于存在并发限制,需要限制 request 创建个数,避免页面卡死。

  3. 在上传大文件时,应提供适当的进度反馈和错误处理以确保良好的用户体验。

  4. 对于文件切片、并发上传和断点续传,后端需要能够接受文件片段,并能够处理并发请求和断点数据,因此需要合后端人员密切配合。

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

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

相关文章

康师傅JAVA核心内容

链接&#xff1a;康师傅JAVA核心内容 (qq.com)

黑龙江等保测评如何做到既全面又高效?

在黑龙江省进行等保测评&#xff0c;必须在全面和高效之间寻求一个平衡点&#xff0c;以保证网络的安全性和可靠性。黑龙江等保测评怎样才能在二者之间发现黄金交汇点&#xff1f;下面&#xff0c;我们来揭开谜底。 精准定位&#xff0c;明确测评范围 首先&#xff0c;一个综…

Docker与LXC差异以及相关命令

容器&#xff1a;Docker与LXC差异以及相关命令 ​ LXC与Docker对比&#xff0c;LXC只实现了进程沙盒化&#xff0c;不支持在不同的机器上进行移植&#xff1b;Docker将应用的所有配置和环境进行了抽象&#xff0c;打包到一个容器中&#xff0c;此容器可以在任何安装了docker的…

vscode搭建rust开发环境

由于rustrover不是免费的&#xff0c;此处教学搭建一套基于vscode的rust开发环境&#xff0c;可运行&#xff0c;可调式 1.下载vscode1.91.1 Download Visual Studio Code - Mac, Linux, Windows 2.下载插件 打开网站下载插件 rust-analyzer-0.4.2049、vscode-lldb-1.10.0、…

IDEA项目的依赖(pom.xml文件)导入问题及解决

前言&#xff1a;该文章为转载&#xff0c;没有仔细的看 IDEA新建项目和pom.xml文件被修改时&#xff0c;右下角都会出现 Maven projects need to be imported&#xff08;项目需要导入依赖&#xff09; 如下&#xff0c;点击 Import Changes导入后&#xff0c;有时会一直处于…

NAS、SAN 与 DAS 的比较与应用场景

文章目录 1. NAS&#xff08;网络附加存储&#xff09;定义特点实现成本&#xff1a;适用场景 2. SAN&#xff08;存储区域网络&#xff09;定义特点实现成本&#xff1a;适用场景 3. DAS&#xff08;直接附加存储&#xff09;定义特点实现成本&#xff1a;适用场景 区别总结结…

Redis学习[1] ——基本概念和数据类型

Redis学习[1] ——基本概念和数据类型 一、Redis基础概念 1.1 Redis是什么&#xff0c;有什么特点&#xff1f; Redis是一个基于**内存的数据库&#xff0c;因此读写速度非常快**&#xff0c;常用作缓存、消息队列、分布式锁和键值存储数据库。支持多种数据结构&#xff1a;…

Java 内推 | 教育行业缺口来了,研发,运维,产品,教研,职能,营销... 别错过

Java 内推 | 教育行业缺口来了&#xff0c;研发&#xff0c;运维&#xff0c;产品&#xff0c;教研,职能&#xff0c;营销… 别错过 岗位职责&#xff1a; 1、根据公司战略及业务规划&#xff0c;参与部门业务架构分析与设计&#xff0c;包含规划立足当前、面向未来的应用架构…

源码编译安装,及nginx服务控制、监控块

1.源码编译安装&#xff1a; [root17dns ~]# wget https://nginx.org/download/nginx-1.27.0.tar.gz 2.解压&#xff1a; [root17dns ~]# tar -zxvf nginx-1.27.0.tar.gz 3.安装gcc等工具 [root17dns ~]# yum -y install gcc gcc-c [root17dns ~]# yum -y install make lrzsz …

postman给全部接口添加请求头数据(如token)

如果给没有一个接口添加请求头token就太慢了&#xff0c;如下图。可以点击所有接口的所属的目录。点击“Scripts”&#xff0c;点击Pre-request按钮。加入代码&#xff1a; pm.request.addHeader("Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI111pXVCJ9.eyJjbGFpbXMiOnsiaW…

小红书笔记评论采集全攻略:三种高效方法教你批量导出

摘要&#xff1a; 本文将深入探讨如何利用Python高效采集小红书平台上的笔记评论&#xff0c;通过三种实战策略&#xff0c;手把手教你实现批量数据导出。无论是市场分析、竞品监测还是用户反馈收集&#xff0c;这些技巧都将为你解锁新效率。 一、引言&#xff1a;小红书数据…

项目实战_表白墙(简易版)

你能学到什么 一个比较简单的项目&#xff1a;表白墙&#xff08;简易版&#xff09;&#xff0c;浏览器&#xff1a;谷歌升级版将在下个博客发布 效果如下 正文 说明 我们是从0开始一步一步做这个项目的&#xff0c;里面的各种问题&#xff0c;我也会以第一人称视角来解…

yolov10在地平线旭日X3派上的部署和测试(Python版本和C++版本)

0、搭建开发环境 当前的测试根据一下的步骤并修改源码是可以实现yolov8的板端运行&#xff0c;如果不想再搭建环境和测试代码bug上浪费更多的时间可以直接获取本人的测试虚拟机&#xff0c;所有的测试代码、虚拟环境和板端测试工程以全部打包到了虚拟机&#xff0c;需要的可以…

OpenBayes 教程上新 | 文生图、图生图、图像修复三合一神器, HiDiffusion 一键启动教程现已上线!

扩散模型已成为高分辨率图像合成的主流方法&#xff0c;传统的扩散模型虽然在图像合成方面取得了显著进展&#xff0c;但在扩展到更高分辨率时往往面临对象重复和计算成本增加的问题。 旷世科技开源的高分辨率框架 HiDiffusion&#xff0c;由分辨率感知 U-Net (RAU-Net) 和改进…

求最大公约数与最小公倍数(C语言 简洁快速版)

求最大公约数 辗转相除法&#xff08;首选&#xff0c;因为简单快捷&#xff09; 思路&#xff1a; 1.将两整数求余 a%b c 2.如果c 0;则b为最大公约数 3.如果c ! 0,则 a b&#xff1b;b c&#xff1b;继续从1开始执行 4.也就是说该循环的是否继续的判断条件就是c是否为0 …

3.4、图

图的介绍 图也是一种非线性结构&#xff0c;图中任意两个节点间都可能有直接关系。相关定义如下&#xff1a; 无向图&#xff1a;图的结点之间连接线是没有箭头的&#xff0c;不分方向。 有向图&#xff1a;图的结点之间连接线是箭头&#xff0c;区分A到B,和B到A是两条线。 …

Codeforces 962 div3 A-F

A 题目分析 签到 C代码 #include<iostream> using namespace std; int main(){int t;cin>>t;while(t--){int n;cin>>n;cout<<n/4n%4/2<<endl;} } B 题目分析 将n*n的方格分成若干个k*k的方格&#xff0c;每个k*k的方格中所有的数都相同 遍历…

SwiftUI知识点(四)

NavigationStack import SwiftUIstruct NavigationStackBootcamp: View {let fruits ["Apple", "Orange", "Banana"]///路径数组State private var stackPath: [String] []var body: some View {NavigationStack(path: $stackPath) {VStack{B…