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.rhkb.cn/news/387187.html

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

相关文章

贪心算法-买卖股票问题

贪心算法&#xff08;Greedy Algorithm&#xff09;是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法。贪心算法并不保证总是能得到全局最优解&#xff0c;但它通常能得到不错的解…

【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析

前言&#xff1a; 接上篇&#xff0c;排序算法除了选择排序&#xff08;希尔排序&#xff09;和插入排序&#xff08;堆排序&#xff09;之外&#xff0c;还用交换排序&#xff08;冒泡排序、快速排序&#xff09;和归并排序已经非比较排序&#xff0c;本篇来深层解析这些排序算…

Java 基础 and 进阶面试知识点(超详细)

一个 Java 文件中是否可以存在多个类&#xff08;修饰类除外&#xff09;&#xff1f; 一个 Java 文件中是可以存在多个类的&#xff0c;但是一个 Java 文件中只能存在一个 public 所修饰的类&#xff0c;而且这个 Java 文件的文件名还必须和 public 所修饰类的类名保持一致&a…

轻松入门Linux—CentOS,直接拿捏 —/— <1>

一、什么是Linux Linux是一个开源的操作系统&#xff0c;目前是市面上占有率极高的服务器操作系统&#xff0c;目前其分支有很多。是一个基于 POSIX 和 UNIX 的多用户、多任务、支持多线程和多 CPU 的操作系统 Linux能运行主要的UNIX工具软件、应用程序和网络协议 Linux支持 32…

C++入门基础:C++中的循环语句

循环语句是编程语言中用来重复执行一段代码直到满足特定条件的一种控制结构。它们对于处理需要重复任务的场景非常有用&#xff0c;比如遍历数组、累加数值、重复执行某项操作直到满足条件等。 但是在使用循环语句的时候需要注意下哈&#xff0c;有时候一不小心会构成死循环或者…

centos安装kubernetes

本章程安装k8s 1.30版本为例。 1、环境配置 k8s 自1.24版本起&#xff0c;移除了dockershim了&#xff0c;1.30使用了containerd运行部署&#xff0c;containerd部署文档参考centos安装containerd-CSDN博客 k8s部署环境可参考容器运行时 | Kubernetes 1.1、修改主机名称 #…

【Django5】模型定义与使用

系列文章目录 第一章 Django使用的基础知识 第二章 setting.py文件的配置 第三章 路由的定义与使用 第四章 视图的定义与使用 第五章 二进制文件下载响应 第六章 Http请求&HttpRequest请求类 第七章 会话管理&#xff08;Cookies&Session&#xff09; 第八章 文件上传…

MacOS 使用DBeaver连接MySQL数据库 以及常见的问题

文章目录 1 DBeaver介绍2 下载安装3 连接MySQL4 DBeaver使用中的常见问题1 DBeaver驱动无法下载2 连接mysql时报错 Public Key Retrieval is not allowed3 mysql出现错误提示&#xff1a;connection refused: Communications link failure The last packet sent successfully t…

【JavaScript】详解Day.js:轻量级日期处理库的全面指南

文章目录 一、Day.js简介1. 什么是Day.js&#xff1f;2. 安装Day.js 二、Day.js的基本用法1. 创建日期对象2. 格式化日期3. 解析日期字符串4. 操作日期5. 比较日期 三、Day.js的高级功能1. 插件机制2. 国际化支持 四、实际应用案例1. 事件倒计时2. 日历应用 在JavaScript开发中…

界面控件Telerik UI for WPF 2024 Q2亮点 - 全新的AIPrompt组件

Telerik UI for WPF拥有超过100个控件来创建美观、高性能的桌面应用程序&#xff0c;同时还能快速构建企业级办公WPF应用程序。UI for WPF支持MVVM、触摸等&#xff0c;创建的应用程序可靠且结构良好&#xff0c;非常容易维护&#xff0c;其直观的API将无缝地集成Visual Studio…

vite tsx项目的element plus集成 - 按需引入踩坑

前面我们进行了开源组件的自研&#xff0c;很多组件可直接用现成的开源组件库&#xff0c;并不需要自己重复造轮子&#xff0c;为此我们讲如何在当前vite vitepress tsx技术整合的项目中实现element plus组件的按需引入&#xff0c;同时解决遇到的一些坑。 安装Element Plus…

Codeforces Round #956 (Div. 2) and ByteRace 2024

A.思维&#xff1a;https://codeforces.com/contest/1983/problem/A AC代码&#xff1a; #include<bits/stdc.h> using namespace std; int t; int n; int main(){cin>>t;while(t--){cin>>n;for(int i1;i<n;i) cout<<i<<" ";cout…

《浅谈如何培养树立正确的人工智能伦理观念》

目录 摘要&#xff1a; 一、引言 二、《机械公敌》的情节与主题概述 三、人工智能伦理与法律问题分析 1.伦理挑战 2.法律问题 四、培养正确的人工智能伦理观念的重要性 五、培养正确的人工智能伦理观念的途径与方法 1.加强教育与宣传 2.制定明确的伦理准则和规范 3.…

Doris全方位教程+应用实例

Impala性能稍领先于presto,但是presto在数据源支持上非常丰富&#xff0c;包括hive、图数据库、传统关系型数据库、Redis等 缺点&#xff1a;这两种对hbase支持的都不好&#xff0c;presto 不支持&#xff0c;但是对hdfs、hive兼容性很好&#xff0c;其实这也是顺理成章的&…

Swift学习入门,新手小白看过来

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。 &#x1f60a; 座右铭&#xff1a;不…

java-数据结构与算法-02-数据结构-06-双端队列

1. 概述 双端队列、队列、栈对比 注1&#xff1a; Java 中 LinkedList 即为典型双端队列实现&#xff0c;不过它同时实现了 Queue 接口&#xff0c;也提供了栈的 push pop 等方法 注2&#xff1a; 不同语言&#xff0c;操作双端队列的方法命名有所不同&#xff0c;参见下表 接…

day05 Router、vuex、axios

配置 router和vuex需要在创建vue项目的时候&#xff0c;开始的时候选择Manually select features&#xff0c;于是就可以在下一个创建配置讯问中选择router和vuex。 axios则需要执行命令行&#xff1a; npm install axios -S 之后再在需要发送请求的view导入即可。 router…

Chapter 20 Python包

欢迎大家订阅【Python从入门到精通】专栏&#xff0c;一起探索Python的无限可能&#xff01; 文章目录 前言一、自定义包1. 什么是Python包&#xff1f;2. 目录结构3. 导入方式4. __all__变量 二、第三方包1. 什么是第三方包&#xff1f;2. 安装第三方包 前言 在 Python 中&am…

PHP反序列化漏洞

一.PHP的序列化和反序列化 &#xff08;1&#xff09;.作用 PHP的序列化和反序列化是PHP中用于存储或传输PHP的值的一个过程。序列化是将变量转换为可存储或传输的字符串的过程&#xff0c;而反序列化则是将这些字符串转换回PHP变量的过程。这两个过程在PHP开发中非常有用&am…

vue element-ui日期控件传参

前端&#xff1a;Vue element-ui <el-form-item label"过期时间" :rules"[ { required: true, message: 请选择过期时间, trigger: blur }]"><el-date-picker v-model"form.expireTime" type"date" format"yyyy-MM-dd&…