【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传

目录

中等文件代理服务器放行:10MB为单位

proxy

nginx

大文件切片:100MB为单位

断点:存储切片hash

前端方案A

localstorage

后端方案B

服务端

上传

前端

后端

下载

前端

后端

多个大文件传输:spark-md5

哈希碰撞

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求

中等文件代理服务器放行:10MB为单位

proxy

proxy_buffering来控制是否启用代理缓冲,

proxy_buffer_sizeproxy_buffers来调整缓冲区的大小

nginx

在nginx.conf配置文件中,找到或添加一个 httpserverlocation 块,具体位置取决于希望修改的范围。在该块中,添加或修改 client_max_body_size 指令

http {
    ...
    server {
        ...
        location /upload {
            client_max_body_size 100M;
            ...
        }
        ...
    }
    ...
}

检查配置文件是否有语法错误:

sudo nginx -t

如果没有报告错误,重新加载Nginx以使配置更改生效:

sudo systemctl reload nginx

React版本见:前端文件流、切片下载和上传:优化文件传输效率与用户体验 - 掘金

<pre> 标签可定义预格式化的文本。

<pre> 标签的一个常见应用就是用来表示计算机的源代码

Blob(Binary Large Object)对象:存储二进制数据

ArrayBuffer 对象类型:缓存二进制数据

大文件切片:100MB为单位

每个片段大小通常在几百KB到几MB之间

 

断点:存储切片hash

前端方案A

localstorage

  1. 容量限制: 不同浏览器可能有不同的限制,但通常容量限制在 5MB 到 10MB 之间。用于存储断点下标够用

  2. 遵循同源策略

  3. 持久性: 关闭后也存在,只有用户主动清除浏览器缓存或使用代码删除数据,

  4. 访问同步,在读取或写入大量数据时,可能阻塞

  5. 数据类型: string

  6. 适用场景:容量小,非敏感,持久性数据。如果需要处理更大容量的数据,或者需要在不同域之间共享数据,可以考虑 IndexedDB 或服务器端存储。

这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

后端方案B

服务端

前端方案有一个缺陷,如果换了个浏览器就localstorage就失效了,所以推荐后者

上传

前端

<template><div><input type="file" @change="handleFileChange" /><button @click="startUpload">Start Upload</button></div>
</template><script>
export default {data() {return {file: null,chunkSize: 1024 * 1024, // 1MBtotalChunks: 0,uploadedChunks: [],};},methods: {handleFileChange(event) {this.file = event.target.files[0];},startUpload() {if (this.file) {this.totalChunks = this.getTotalChunks();this.uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];this.uploadChunks(0);}},uploadChunks(startChunk) {if (startChunk >= this.totalChunks) {console.log('Upload complete');localStorage.removeItem('uploadedChunks'); return;}//模拟每次至多发起5个并发请求,实际开发中根据请求资源的限定决定?const endChunk = Math.min(startChunk + 5, this.totalChunks);const uploadPromises = [];for (let chunkIndex = startChunk; chunkIndex < endChunk; chunkIndex++) {if (!this.uploadedChunks.includes(chunkIndex)) {const startByte = chunkIndex * this.chunkSize;const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.file.size);const chunkData = this.file.slice(startByte, endByte);const formData = new FormData();formData.append('chunkIndex', chunkIndex);formData.append('file', chunkData);uploadPromises.push(fetch('/upload', {method: 'POST',body: formData,}));}}Promise.allSettled(uploadPromises).then(() => {const newUploadedChunks = Array.from(new Set([...this.uploadedChunks, ...Array.from({ length: endChunk - startChunk }, (_, i) => i + startChunk)]));this.uploadedChunks = newUploadedChunks;localStorage.setItem('uploadedChunks', JSON.stringify(this.uploadedChunks));this.uploadChunks(endChunk);}).catch(error => {console.error('Error uploading chunks:', error);});},getTotalChunks() {return Math.ceil(this.file.size / this.chunkSize);},},
};
</script>

后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');app.use(express.json());
app.use(express.static(chunkDirectory));const storage = multer.diskStorage({destination: chunkDirectory,filename: (req, file, callback) => {callback(null, `chunk_${req.body.chunkIndex}`);},
});const upload = multer({ storage });app.post('/upload', upload.single('file'), (req, res) => {const { chunkIndex } = req.body;console.log(`Uploaded chunk ${chunkIndex}`);res.sendStatus(200);
});app.listen(3000, () => {console.log('Server started on port 3000');
});

下载

前端

<template><div><button @click="startDownload">Start Download</button></div>
</template><script>
import { saveAs } from 'file-saver';export default {data() {return {totalChunks: 0,chunkSize: 1024 * 1024, // 默认1MfileNm: "file.txt",downloadedChunks: [],chunks: [], // 存储切片数据concurrentDownloads: 5, // 并发下载数量};},methods: {startDownload() {this.fetchMetadata();},fetchMetadata() {fetch('/metadata').then(response => response.json()).then(data => {this.totalChunks = data.totalChunks;this.chunkSize = data.chunkSize;this.fileNm = data.fileNm;this.continueDownload();}).catch(error => {console.error('Error fetching metadata:', error);});},async continueDownload() {const storedChunks = JSON.parse(localStorage.getItem('downloadedChunks')) || [];this.downloadedChunks = storedChunks;const downloadPromises = [];let chunkIndex = 0;while (chunkIndex < this.totalChunks) {const chunkPromises = [];for (let i = 0; i < this.concurrentDownloads; i++) {if (chunkIndex < this.totalChunks && !this.downloadedChunks.includes(chunkIndex)) {chunkPromises.push(this.downloadChunk(chunkIndex));}chunkIndex++;}await Promise.allSettled(chunkPromises);}
// 当所有切片都下载完成时 合并切片this.mergeChunks();},downloadChunk(chunkIndex) {return new Promise((resolve, reject) => {const startByte = chunkIndex * this.chunkSize;const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.totalChunks * this.chunkSize);//我不太清楚实际开发中切片是靠idx,还是startByte、endByte,还是两者都用....fetch(`/download/${chunkIndex}?start=${startByte}&end=${endByte}`).then(response => response.blob()).then(chunkBlob => {this.downloadedChunks.push(chunkIndex);localStorage.setItem('downloadedChunks', JSON.stringify(this.downloadedChunks));this.chunks[chunkIndex] = chunkBlob; // 存储切片数据resolve();}).catch(error => {console.error('Error downloading chunk:', error);reject();});});},mergeChunks() {const mergedBlob = new Blob(this.chunks);// 保存合并后的 Blob 数据到本地文件saveAs(mergedBlob, this.fileNm);// 清空切片数据和已下载切片的 localStoragethis.chunks = [];localStorage.removeItem('downloadedChunks');},},
};
</script>

后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');app.use(express.json());app.get('/metadata', (req, res) => {const filePath = path.join(__dirname, 'file.txt'); const chunkSize = 1024 * 1024; // 1MBconst fileNm='file.txt';const fileStats = fs.statSync(filePath);const totalChunks = Math.ceil(fileStats.size / chunkSize);res.json({ totalChunks, chunkSize, fileNm });
});app.get('/download/:chunkIndex', (req, res) => {const chunkIndex = parseInt(req.params.chunkIndex);const chunkSize = 1024 * 1024; // 1MBconst startByte = chunkIndex * chunkSize;const endByte = (chunkIndex + 1) * chunkSize;const filePath = path.join(__dirname, 'file.txt'); fs.readFile(filePath, (err, data) => {if (err) {res.status(500).send('Error reading file.');} else {const chunkData = data.slice(startByte, endByte);res.send(chunkData);}});
});app.listen(3000, () => {console.log('Server started on port 3000');
});

多个大文件传输:spark-md5

MD5(Message Digest Algorithm 5):哈希函数

若使用 文件名 + 切片下标 作为切片 hash,这样做文件名一旦修改就失去了效果,

所以应该用spark-md5根据文件内容生成 hash

webpack 的contenthash 也是基于这个思路实现的

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

// /public/hash.js// 导入脚本
self.importScripts("/spark-md5.min.js");// 生成文件 hash
self.onmessage = e => {const { fileChunkList } = e.data;const spark = new self.SparkMD5.ArrayBuffer();let percentage = 0;let count = 0;// 递归加载下一个文件块const 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.length;self.postMessage({percentage});// 递归调用以加载下一个文件块loadNext(count);}};};// 开始加载第一个文件块loadNext(0);
};
  1. 切片hash/传输等目的都是为了

  2. 内存效率: 对于大文件,一次性将整个文件加载到内存中可能会导致内存占用过高,甚至造成浏览器崩溃。通过将文件切成小块,在处理过程中只需要操作单个块,减小了内存的压力。

  3. 性能优化: 如果直接将整个文件传递给哈希函数,可能会导致计算时间较长,尤其是对于大文件。分成小块逐个计算哈希值,可以并行处理多个块,提高计算效率。

  4. 错误恢复: 在上传或下载过程中,网络中断或其他错误可能会导致部分文件块没有传输成功。通过分块计算哈希,你可以轻松检测到哪些块没有正确传输,从而有机会恢复或重新传输这些块。

  5. // 生成文件 hash(web-worker)
    calculateHash(fileChunkList) {return new Promise(resolve => {// 创建一个新的 Web Worker,并加载指向 "hash.js" 的脚本this.container.worker = new Worker("/hash.js");// 向 Web Worker 发送文件块列表this.container.worker.postMessage({ fileChunkList });// 当 Web Worker 发送消息回来时触发的事件处理程序this.container.worker.onmessage = e => {const { percentage, hash } = e.data;// 更新 hash 计算进度this.hashPercentage = percentage;if (hash) {// 如果计算完成,解析最终的 hash 值resolve(hash);}};});
    },// 处理文件上传的函数
    async handleUpload() {if (!this.container.file) return;// 将文件划分为文件块列表const fileChunkList = this.createFileChunk(this.container.file);// 计算文件 hash,并将结果存储在容器中this.container.hash = await this.calculateHash(fileChunkList);// 根据文件块列表创建上传数据对象this.data = fileChunkList.map(({ file, index }) => ({fileHash: this.container.hash,chunk: file,hash: this.container.file.name + "-" + index,percentage: 0}));// 上传文件块await this.uploadChunks();
    }
    

哈希碰撞

输入空间通常大于输出空间,无法完全避免碰撞

哈希(A) = 21 % 10 = 1

哈希(B) = 31 % 10 = 1

所以spark-md5 文档中要求传入所有切片并算出 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求

面试官桀桀一笑:你没做过大文件上传功能?那你回去等通知吧! - 掘金

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

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

相关文章

什么是Pytorch?

当谈及深度学习框架时&#xff0c;PyTorch 是当今备受欢迎的选择之一。作为一个开源的机器学习库&#xff0c;PyTorch 为研究人员和开发者们提供了一个强大的工具来构建、训练以及部署各种深度学习模型。你可能会问&#xff0c;PyTorch 是什么&#xff0c;它有什么特点&#xf…

JQuery快速入门教程

1、JQuery快速入门 1.1、JQuery介绍 jQuery 是一个 JavaScript 库。所谓的库&#xff0c;就是一个 JS 文件&#xff0c;里面封装了很多预定义的函数&#xff0c;比如获取元素&#xff0c;执行隐藏、移动等&#xff0c;目的就 是在使用时直接调用&#xff0c;不需要再重复定义…

微机原理与接口技术 学习笔记(二) 存储器

文章目录 一&#xff0c;存储器1.1 概述1.1.1 半导体存储器的分类按制造工艺&#xff1a; 易失性或挥发性存储器 / 不易失性或不挥发性存储器按制造工艺&#xff1a; 1.1.2 半导体存储器的性能指标1.1.3 半导体存储器的一般结构及组成 1.2 随机存取存储器 RAM1.2.1 静态RAM1.2.…

UML图绘制 -- 类图

1.类图的画法 类 整体是个矩形&#xff0c;第一层类名&#xff0c;第二层属性&#xff0c;第三层方法。 &#xff1a;public- : private# : protected空格: 默认的default 对应的类写法。 public class Student {public String name;public Integer age;protected I…

机器学习知识点总结:什么是GBDT(梯度提升树)

什么是GBDT(梯度提升树) 虽然GBDT同样由许多决策树组成&#xff0c;但它与随机森林由许多不同。 其中之一是GBDT中的树都是回归树&#xff0c;树有分类有回归&#xff0c;区分它们的方法很简单。将苹果单纯分为好与坏的是分类树&#xff0c;如果能为苹果的好坏程度打个分&…

pycharm上传项目到github,版本管理

前提&#xff1a;下载git 设置Git路径 登录Github 此时自动打开浏览器&#xff0c;并打开连接页面&#xff0c;点击 Authorize GitHub。登录&#xff1a; 创建本地仓库 提交到Github 填写初始提交相关信息 origin&#xff0c;它们只是远程服务器的一个别名&#xff0c;否则你就…

小程序前台Boot后台校园卡资金管理系统java web学校进销存食堂挂失jsp源代码

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 小程序前台Boot后台校园卡资金管理系统 系统有2权限&…

React Native expo项目修改应用程序名称

https://expo.dev/accounts/xutongbao/projects npm install --global eas-cli && \eas init --id e32cf2c0-da5b-4a65-814a-4958d58f0ca7 eas init --id e32cf2c0-da5b-4a65-814a-4958d58f0ca7 app.config.js: export default {name: 学习,slug: learn-gpt,owner: x…

虹科方案 | 汽车总线协议转换解决方案

虹科提供的汽车总线协议转换解决方案&#xff0c;有效地解决了车载ECU、总线测试工具等&#xff0c;因通信协议不兼容而无法通信的问题&#xff0c;并在优化测试台架、模拟总线信号等方向有显著的成效。 文章目录 前言一、应用场景1&#xff1a;跨协议通信测试BMS&#xff08;电…

Kotlin 基础教程二

constructor 构造器一般情况下可以简化为主构造器 即: class A constructor(参数) : 父类 (参数) 也可以在构造器上直接声明属性constructor ( var name) 这样可以全局访问 init { } 将和成员变量一起初始化 thread {} ktx 默认创建一个线程 susped 挂起 data class 可以简…

webSocket 聊天室 node.js 版

全局安装vue脚手架 npm install vue/cli -g 创建 vue3 ts 脚手架 vue create vue3-chatroom 后端代码 src 同级目录下建 server: const express require(express); const app express(); const http require(http); const server http.createServer(app);const io req…

数学建模的概念和学习方法(什么是数学建模)

一、初步认识数学建模 数学建模是将数学方法和技巧应用于实际问题的过程。它涉及使用数学模型来描述和分析现实世界中的现象、系统或过程&#xff0c;并通过数学分析和计算来预测、优化或解决问题。数学建模可以应用于各种领域&#xff0c;包括自然科学、工程、经济学、环境科学…

rust入门系列之Rust介绍及开发环境搭建

Rust教程 Rust基本介绍 网站: https://www.rust-lang.org/ rust是什么 开发rust语言的初衷是&#xff1a; 在软件发展速度跟不上硬件发展速度&#xff0c;无法在语言层面充分的利用硬件多核cpu不断提升的性能和 在系统界别软件开发上&#xff0c;C出生比较早&#xff0c;内…

vue3.0 element-plus 不同版本 el-popover 循环优化

表格内循环el-popover 渲染以后的页面&#xff0c;数据量很大的时候页面会卡&#xff0c;生成的代码&#xff1a; 解决思路&#xff1a;将el-popover提出来&#xff0c;不参与循环&#xff0c;让el-popover只渲染一次 1、以1.1.0-beta.24版为例&#xff08;低版本&#xff09;…

从关键新闻和最新技术看AI行业发展(2023.7.10-7.23第三期) |【WeThinkIn老实人报】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【WeThinkIn老实人报】本栏目旨在整理&挖掘AI行业的关键新闻和最新技术&#xff0c;同时Rocky会对这些关键信息进行解读&#xff0c;力求让读者们能从容跟随AI科技潮流。也欢迎大家提出宝贵的优化建议&#xff0c;一起交流…

保险龙头科技进化论:太保的六年

如果从2013年中国首家互联网保险公司——众安在线的成立算起&#xff0c;保险科技在我国的发展已走进第十个年头。十年以来&#xff0c;在政策指引、技术发展和金融机构数字化转型的大背景下&#xff0c;科技赋能保险业高质量发展转型已成为行业共识。 大数据、云计算、人工智…

Linux笔试题(4)

67、在局域网络内的某台主机用ping命令测试网络连接时发现网络内部的主机都可以连同,而不能与公网连通,问题可能是__C_ A.主机ip设置有误 B.没有设置连接局域网的网关 C.局域网的网关或主机的网关设置有误 D.局域网DNS服务器设置有误 解析&#xff1a;在局域网络内的某台主…

Python爬虫的scrapy的学习(学习于b站尚硅谷)

目录 一、scrapy  1. scrapy的安装  &#xff08;1&#xff09;什么是scrapy  &#xff08;2&#xff09;scrapy的安装 2. scrapy的基本使用  &#xff08;1&#xff09;scrap的使用步骤  &#xff08;2&#xff09;代码的演示 3. scrapy之58同城项目结构和基本方法&…

2023 最新 小丫软件库app开源源码 PHP后端

上传了源码解压之后&#xff0c;在admin/public/config.php修改后台登录账号和密码 后台地址&#xff1a;域名或者ip/admin 然后自己修改配置即可 后端搭建完成&#xff0c;现在导入iapp源码 导入iapp源码之后&#xff0c;修改mian.iyu载入事件的对接api和url就可以打包了 sss …

【OpenVINOSharp】在英特尔® 开发者套件爱克斯开发板使用OpenVinoSharp部署Yolov8模型

在英特尔 开发者套件爱克斯开发板使用OpenVinoSharp部署Yolov8模型 一、英特尔开发套件 AIxBoard 介绍1. 产品定位2. 产品参数3. AI推理单元 二、配置 .NET 环境1. 添加 Microsoft 包存储库2. 安装 SDK3. 测试安装4. 测试控制台项目 三、安装 OpenVINO Runtime1. 下载 OpenVINO…