首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

大文件上传

  • 25-04-16 23:22
  • 2106
  • 11342
juejin.cn

业务场景

当我们上传一个文件,当文件比较大的时候,需要上传的时间比较长,如果出现网络中断的场景,那么就需要重新的上传整个文件,这对于用户来说,是非常不好的体验,也非常浪费资源。 所以在大文件上传时,往往对 大文件 进行切片操作,前端负责将文件进行切片,然后单文件上传给后端,等后端拿到所有的切片,进行一个合并。

  1. 用户选择文件后点击上传按钮。
  2. 前端计算文件的哈希值。
  3. 检查文件是否已存在(秒传检查)。
  4. 如果文件不存在,将文件切片。
  5. 获取已上传的切片,生成待上传的切片队列。
  6. 控制并发数量上传切片。
  7. 所有切片上传完成后,合并切片。

整体流程

切片上传是如何实现的:

1.选择文件:用户在前端页面选择要上传的文件,将选择的文件进行保存

2.利用sparkmd5计算文件的哈希值:用于后续的秒传判断。

3.利用文件的slice方法,把一个大文件切片成很多个小文件push到一个数组里

4.利用form data,循环把每个切片都传递给后端(文件切片,文件名,索引值,总的切片数量)

5.判断已经上传的切片数量和总的切片数量是否相等,如相等,向后端发送合并文件的请求;

利用索引值来确定切片的顺序,又利用哈希值来验证文件唯一性,以提高文件上传系统的可靠性和安全性。

前端最核心的操作就是:如何对文件进行切片?

1.选择文件:用户在前端页面选择要上传的文件,将选择的文件进行保存。拿到的是一个文件对象。切片是一个blob 对象

利用文件的slice方法,把一个大文件切片成很多个小文件push到一个数组里

切片方法:传入的是文件 和每一个切片的大小。创建一个数组,用来保存每个切片。

秒传场景:刷新一下页面,问服务器,当前文件上传没有,哪些切片还没有上传

文件hash值的计算 异步封装

重点在于:怎么知道是这个文件,用一个东西去描述这个文件,

不能用文件名:文件名容易重复,路径会移动

找到唯一能代表这个文件的东西:文件hash,hash算法:将任何数据转化为一个固定长度的字符串,也是不可逆的。而且,里面任意数据的修改,都会导致产生不同的哈希值。

Md5算法  :如何在客户端去计算这个哈希值,使用的是第三方库 spark-md5

重点:不能一次性计算整个文件的哈希值,因为计算文件的哈希值,要拿到文件的整个数据,一个高清电影可能就100G了,内存拿到数据再去计算,吃不消。

要分块去算,增量算法,先拿一块数据计算,先计算这块,这块计算完了就不要了,下一块来了,和之前的结果计算一个新的结果。这块数据也不要了。

注意点:将文件hash值的计算,封装成异步的,需要花费一点时间去计算哈希值

尽管这样,还是可能卡顿:优化方案,使用webworker 去单独开一个线程

断点续传的实现

若不能秒传,向后端发送包含文件哈希值的请求,获取已上传的切片索引数组。前端根据这个数组确定哪些切片还需要上传。遍历切片的数组,如果当前切片的索引包含在已上传的数组中,就会跳过这个切片,进行下一个切片的判断。通过索引记录已上传的切片,重新上传时跳过已上传的部分来实现断点续传功能

秒传如何实现

上传前先发送文件 hash,检查是否存在相同哈希值且已完成上传的文件,服务端存在相同文件则直接返回,使用 spark-md5 计算文件 hash。

并发上传是通过以下方式实现的

并发上传指同时上传文件的多个切片以提升效率,实现方式多样:

  1. 队列方式: - 原理:利用队列存储待上传切片,结合计数器和并发限制值控制并发数量。上传任务完成后,从队列取新切片上传。 - 优点:逻辑清晰,能灵活控制并发数量。

  2. Promise.all 或 Promise.allSettled: - 原理:将切片封装成 Promise 对象,使用上述方法同时发起多个请求,需提前控制并发数量。 - 优点:代码简洁,适合批量处理请求。 3. 第三方库(如 p - limit): - 原理:借助库提供的并发控制功能,轻松管理并发数量。 - 优点:使用方便,减少手动控制复杂度。

  3. 分片队列:

  • 使用 chunkQueue 数组存储所有分片的索引。
  • 每次从队列中取出一个分片进行上传。
javascript
代码解读
复制代码
const chunkQueue = Array.from({ length: totalChunks }, (_, i) => i);
  1. 上传任务函数:
  • 定义了 uploadNextChunk 函数,用于从队列中取出一个分片并上传。
  • 每次上传完成后,递归调用自身以继续处理下一个分片。
javascript
代码解读
复制代码
const uploadNextChunk = async () => { if (chunkQueue.length === 0) return; // 队列为空时结束 const chunkIndex = chunkQueue.shift(); // 从队列中取出一个分片索引 // 上传逻辑... await uploadNextChunk(); // 继续上传下一个分片 };
  1. 并发控制:
  • 使用 maxConcurrentUploads 控制并发数量。
  • 创建多个并发任务,每个任务调用 uploadNextChunk。
  • 使用 Promise.all 等待所有并发任务完成。
javascript
代码解读
复制代码
const uploaders = Array.from({ length: maxConcurrentUploads }, () => uploadNextChunk() ); await Promise.all(uploaders); // 等待所有并发任务完成

通过这种方式,代码能够同时启动多个上传任务,并在任务完成后继续处理队列中的分片,从而实现了并发上传的功能。

前端向后端发送请求的方式

主要借助 axios 库

3.1 检查文件是否已存在(秒传检查)
csharp
代码解读
复制代码
const checkFileExists = async (hash) => { try { // 使用 axios 的 get 方法发送请求,传递文件哈希值作为参数 const response = await axios.get('/checkFile', { params: { hash } }); // 返回后端响应中的 exists 字段 return response.data.exists; } catch (error) { // 若请求出错,打印错误信息并返回 false console.error('检查文件存在性时出错:', error); return false; } };

此函数会向后端 /checkFile 接口发送一个 GET 请求,携带文件的哈希值,后端依据该哈希值判断文件是否已存在,随后返回结果。

3.2 获取已上传的切片
csharp
代码解读
复制代码
const getUploadedChunks = async (hash) => { try { // 使用 axios 的 get 方法发送请求,传递文件哈希值作为参数 const response = await axios.get('/getUploadedChunks', { params: { hash } }); // 返回后端响应中的 chunks 字段 return response.data.chunks; } catch (error) { // 若请求出错,打印错误信息并返回空数组 console.error('获取已上传切片时出错:', error); return []; } };

该函数会向后端 /getUploadedChunks 接口发送一个 GET 请求,携带文件的哈希值,后端返回已上传的切片索引。

3.3 上传单个切片
javascript
代码解读
复制代码
const uploadChunk = async (chunk, index) => { uploadingCount.value++; try { // 创建一个 FormData 对象,用于存储要上传的数据 const formData = new FormData(); // 向 FormData 中添加切片文件 formData.append('file', chunk); // 向 FormData 中添加文件哈希值 formData.append('hash', fileHash.value); // 向 FormData 中添加切片索引 formData.append('index', index); // 使用 axios 的 post 方法发送请求,传递 FormData 对象 await axios.post('/uploadChunk', formData, { onUploadProgress: (progressEvent) => { // 计算上传进度百分比 const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); // 更新上传进度 uploadProgress.value = percentCompleted; }, }); } catch (error) { // 若请求出错,打印错误信息 console.error('上传切片时出错:', error); } finally { // 上传完成后,正在上传的任务数量减 1 uploadingCount.value--; if (uploadQueue.value.length > 0) { // 若队列中还有待上传的切片,继续上传 startUploadQueue(); } else if (uploadingCount.value === 0) { // 若所有切片上传完成,合并切片 mergeChunks(); } } };

这个函数会向后端 /uploadChunk 接口发送一个 POST 请求,使用 FormData 携带切片文件、文件哈希值和切片索引。同时,利用 onUploadProgress 事件监听上传进度。

3.4 合并切片
javascript
代码解读
复制代码
const mergeChunks = async () => { try { // 使用 axios 的 post 方法发送请求,传递文件哈希值 await axios.post('/mergeChunks', { hash: fileHash.value }); // 若合并成功,弹出提示框 alert('文件上传完成'); } catch (error) { // 若请求出错,打印错误信息 console.error('合并切片时出错:', error); } };

该函数会向后端 /mergeChunks 接口发送一个 POST 请求,携带文件的哈希值,通知后端合并所有切片。

4. 总结

前端向后端发送请求的步骤如下:

  1. 引入 axios 库。
  2. 依据不同的业务需求,使用 axios 的 get 或 post 方法发送请求。
  3. 处理请求的响应和错误。 在实际应用中,根据后端接口的具体要求调整请求参数和请求方式。

大文件上传可以优化的地方

使用webworker计算文件的哈希值

webworker知识点

Web Worker 是一种浏览器提供的 API,允许你在一个独立的线程中执行 JavaScript 代码,与主线程(UI 线程)分离。Web Worker 可以处理计算密集型任务,如数据处理、文件解析等,这些任务通常会阻塞主线程,导致 UI 卡顿。通过 Web Worker,你可以将这些耗时操作移到后台线程,确保主线程始终保持响应状态。

工作原理:

  1. 独立线程:Web Worker 在一个与主线程(UI 线程)分离的线程中运行,主线程和 Worker 线程之间通过消息传递(postMessage)进行通信。
  2. 主线程与 Worker 通信:主线程可以通过 postMessage() 方法向 Worker 发送数据,Worker 完成计算后,通过 postMessage() 将结果返回给主线程。
  3. 异步操作:由于 Worker 在后台线程中运行,因此它的执行不会阻塞主线程,所有的计算任务都是异步执行的。
  4. 线程间通信:Worker 无法直接访问主线程的 DOM、window 或者 document 等对象,它只能通过 postMessage() 与主线程进行数据交换。返回的数据是通过事件机制传递的,使用 onmessage 监听数据的返回。

Web Worker 的优势:

  • 性能提升:Web Worker 可以让长时间的计算任务在后台线程中执行,避免 UI 阻塞,提升用户体验。
  • 非阻塞性:主线程可以继续处理用户交互和渲染,而不被复杂计算所阻塞。
  • 多线程处理:对于 CPU 密集型任务,Web Worker 可以将工作分配给多个 Worker,实现并行计算,提高性能。

Web Worker 的应用场景:

  • 大数据处理:例如,处理大量的数组计算、排序、数据筛选等任务。
  • 图像处理:例如,进行图像的处理和转换,而不影响 UI 渲染。
  • 音视频处理:例如,音视频的编码、解码等计算密集型操作。
  • 异步任务:一些需要后台执行的异步任务,可以通过 Worker 来处理。

Web Worker 的局限性:

  • 无法操作 DOM:Web Worker 在独立线程中运行,不能直接访问 DOM 和 window,只能通过消息传递来与主线程交换数据。
  • 数据传递:数据通过 postMessage() 传递时会发生深拷贝,因此传递大数据时可能会有性能开销。
  • 浏览器支持:大多数现代浏览器支持 Web Worker,但在旧版浏览器中可能不被支持。
  1. 创建一个 Web Worker:

    javascript
    代码解读
    复制代码
    // main.js (主线程) const worker = new Worker('worker.js') // 创建 Worker 实例 worker.postMessage('Hello, Worker!') // 向 Worker 发送消息 worker.onmessage = function (event) { console.log('Worker says: ', event.data) // 接收 Worker 的响应 }
  2. Worker 文件(worker.js):

    javascript

    javascript
    代码解读
    复制代码
    // worker.js (Worker 线程) onmessage = function (event) { console.log('Main thread says: ', event.data) postMessage('Hello, Main Thread!') // 发送响应到主线程 }

使用webworker计算文件哈希值

js
代码解读
复制代码
// 计算文件哈希值 const calculateFileHash = (file) => { return new Promise((resolve) => { const worker = new Worker(new URL('./hashWorker.js', import.meta.url)); worker.postMessage(file); worker.onmessage = (e) => { resolve(e.data); }; }); };
js
代码解读
复制代码
importScripts('https://cdn.jsdelivr.net/npm/[email protected]/spark-md5.min.js'); self.onmessage = (e) => { const file = e.data; const chunkSize = 1024 * 1024; // 1MB const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new self.SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = (e) => { spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { const hash = spark.end(); self.postMessage(hash); } }; fileReader.onerror = () => { console.error('读取文件时出错'); }; const loadNext = () => { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(file.slice(start, end)); }; loadNext(); };

参考:JS 基础知识 | 前端面试派 (mianshipai.com)

注:本文转载自juejin.cn的学前端的巧克力的文章"https://juejin.cn/post/7492702134732095498"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top