您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
大文件上传实践分享
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
大文件上传实践分享
jd****
2024-03-04
IP归属:北京
184浏览
# 一、方案背景: 在此前的项目中有个需求是用户需要通过前端页面上传大约1.5G的压缩包,存储到OSS,后提供给其他用户下载。于是我开始了大文件上传方案的探索。本文主要探究的是前端技术实现,后端给予相应的支持。 # 二、 原理探索之路 ## 2.1大文件上传想要实现的目标 **在此项目中,我想实现的目标是** 1. 能够`快速`的将1.5G的文件上传到服务端, 由服务端进行存储,之后提供给其他设备下载。 2. 能够支持在网络条件不好时实现 `断点续传` 。 3. 能够在不同用户上传同一个文件包时执行`秒传`。 ## 2.2 实现思路 1. spark-md5 计算文件的内容`hash`,以此来确定文件的唯一性 2. 将文件`hash`发送到服务端进行查询,以此来确定该文件在服务端的存储情况,这里可以分为三种: 未上传、已上传、上传部分。(前提:分块大小固定) 3. 根据服务端返回的状态执行不同的上传策略: * 已上传: 执行秒传策略,即快速上传(实际上没有对该文件进行上传,因为服务端已经有这份文件了),用户体验下来就是上传得飞快,嗖嗖嗖。。。 * 未上传、上传部分: 执行计算待上传分块的策略 4. 并发上传还未上传的文件分块。 5. 当传完最后一个文件分块时,向服务端发送合并的指令,即完成整个大文件的分块合并,实现在服务端的存储。 **整体流程如下:** ![image.png](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2024-02-27-10-06V7BQWXgonHJCCBA.png) 总结一下:将大文件通过切分成N个小文件,通过并发多个HTTP请求,实现快速上传;在每次上传前计算文件`hash`,带着这个文件`hash`去服务端查询该文件在服务端的存储状态,通过状态来判断需要上传的分块,实现断点续传、秒传。 # 三、实践之路 ## 3.1 文件`hash`计算 本项目中计算文件`hash`的使用`spark-md5`。 ```JavaScript import SparkMD5 from 'spark-md5' const CHUNK_SIZE = 1024 * 1024 * 5 // 5M // 对大文件进行分片 function sliceFile2chunk(file) { const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const fileChunks = [] if (file.size <= CHUNK_SIZE) { fileChunks.push({ file }) } else { let chunkStartIndex = 0 while (chunkStartIndex < file.size) { fileChunks.push({ file: blobSlice(file, chunkStartIndex, chunkStartIndex + CHUNK_SIZE) }) chunkStartIndex = chunkStartIndex + CHUNK_SIZE } } return fileChunks } function getFileHash(file) { let hashProcess = 0 let fileHash = null // 这里需要使用异步执行,保证获取到hash后执行下一步 return new Promise((resolve) => { const fileChunks = sliceFile2Chunk(file) const spark = new SparkMD5.ArrayBuffer() let hadReadChunksNum = 0 const readFile = (chunkIndex) => { const fileReader = new FileReader() fileReader.readAsArrayBuffer(fileChunks[chunkIndex]?.file) fileReader.onload = (e) => { hadReadChunksNum++ spark.append(e.target.result) if (hadReadChunksNum === fileChunks.length) { hashProcess = 100 fileHash = spark.end() fileReader.onload = null resolve(fileHash) } else { hashProcess = Math.floor((hadReadChunksNum / fileChunks.length) * 100); readFile(hadReadChunksNum) } } } readFile(0) }) } // await 用于表示这里是一个异步操作 const fileHash = await getFileHash(file) const fileChunks = sliceFile2chunk(file) ``` 这里将文件`hash`发送给服务端,获取服务端对该文件的存储状态 ```JavaScript // 采用表单形式提交数据,不是必须这样 const fileInfo = new FormData() fileInfo.append('fileHash', fileHash) fileInfo.append('fileName', name) // getFileStatusFn是向服务端请求的文件初始状态的 http 方法, await 标识这里是一个异步请求 const res = await getFileStatusFn(fileInfo) ``` ## 3.2 根据服务端返回的状态执行不同的上传策略 根据服务端返回的状态,来计算出需要上传的文件分块,以分块下标来区分不同的块。 * 0 未上传 * 1 上传部分 * 2 上传完成 ```JavaScript // 这里的 res 是文件在服务端的状态 function createWait2UploadChunks(res) { if (res.data) { const wait2UploadChunks = [] if (res.data.result === 0 ) { // 3.1中得到的文件 chunks fileChunks.forEach((item, index) => { const chunk = formateChunk(item, index) wait2UploadChunks.push(chunk) }, this) } if (res.data.result === 1) { const restFileChunksIndex = [] // tagList 是服务端返回的已上传的文件块标识 类型是Array<Number> res.data.tagList.forEach((item) => { restFileChunksIndex.push(item.index) }, this) fileChunks.forEach((item, index) => { if (!restFileChunksIndex.includes(index)) { const chunk = formateChunk(item, index) wait2UploadChunks.push(chunk) } }) } if(res.data.result === 2) { console.log('执行自定义的秒传操作') } return wait2UploadChunks } } // 该函数式对文件块进行标准化,这里可以与后端做协商得出的,看后端需要什么样的数据 function formateChunk(item, index) { const chunkFormData = new FormData() chunkFormData.append("file", item.file); chunkFormData.append("index", index); chunkFormData.append("partSize", item.file.size); chunkFormData.append("fileHash", fileHash); return chunkFormData } // 入参是 3.2 得到的response, 出参事最终需要上传的分片 const wait2UploadChunks = createWait2UploadChunks(res) ``` ## 3.3 并发上传还未上传的文件分块 这一步主要是将待上传的分块传输到服务端, 这里采用并发5(`页面资源请求时,浏览器会同时和服务器建立多个TCP连接,在同一个TCP连接上顺序处理多个HTTP请求。所以浏览器的并发性就体现在可以建立多个TCP连接,来支持多个http同时请求。Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。`)个HTTP请求的方式进行上传,每当有一个请求完成后就新增一个分块传输请求,确保一直并发5个请求。 ``` const currentHttpNum = 0 const maxHttpNum = 5 const hasUploadedChunkNum = 0 const nextChunkIndex = 4 const uploadProcess = 0 uploadFileChunks() function uploadFileChunks() { wait2UploadChunks.slice(0, maxHttpNum).forEach((item) => { uploadFileChunk(item) }, this) } async function uploadFileChunk(chunkFormData) { try { currentHttpNum++ const res = await uploadChunkFn(chunkFormData) // uploadChunkFn是执行文件上传的HTTP请求 currentHttpNum-- if (res.code === 200) { if (hasUploadedChunkNum < wait2UploadChunks.length) { hasUploadedChunkNum++ } if (wait2UploadChunks.length > ++nextChunkIndex) { uploadFileChunk(wait2UploadChunks[nextChunkIndex]) } uploadProcess = Math.floor((hasUploadedChunkNum / wait2UploadChunks.length) * 100) if (currentHttpNum <= 0) { // 定义在 3.5 mergeChunks() // 第五步执行的函数 } } } catch (error) { console.log(error); } } ``` ## 3.4 向服务端发送合并的指令 当最后一个分块完成传输时,执行合并指令 ``` async mergeChunks() { try { const res = await mergeChunkFn({ //mergeChunkFn 是HTTP请求 fileHash: fileHash, }) } catch (error) { console.log(error); } } ``` # 四、可优化点 ## 4.1 hash计算优化 hash计算可以利用 `web worker` 协程来计算,这里提供一下worker的实现: ```JavaScript // worker.js self.addEventListener('message', function (e) { self.postMessage('You said: ' + e.data); }, false); self.close() // self代表子线程自身,即子线程的全局对象 // 主线程 const worker = new Worker('./worker.js') // 传入的是一个脚本 worker.postMessage('Hello World'); worker.onmessage = function (e) { console.log(e.data); } ``` ## 4.2 分块大小合理化 本项目实测用的5M的分片,具体的环境信息如下: 1. 网络带宽: 10M/s 2. 服务器: 2台 4核32G 各位可根据自己的实际条件,根据网络情况, 合理去制定分块大小。 ## 4.3 多个客户端上传同一个文件包来缩减上传时间 大家可以考虑一下如何通过多个客户端来同时上传一个文件,以此来实现更快的上传? <br> **最后欢迎大家交流学习,优化方案,共同成长。留下你的赞👍🏻** 备注参考资料: [文件上传](https://juejin.cn/post/6844904046436843527#heading-14)
上一篇:学算法要读《算法导论》吗?
下一篇:基于vite多页面实现多端同构开发和部署
jd****
文章数
4
阅读量
84
作者其他文章
01
大文件上传实践分享
一、方案背景:在此前的项目中有个需求是用户需要通过前端页面上传大约1.5G的压缩包,存储到OSS,后提供给其他用户下载。于是我开始了大文件上传方案的探索。本文主要探究的是前端技术实现,后端给予相应的支持。二、 原理探索之路2.1大文件上传想要实现的目标在此项目中,我想实现的目标是能够快速的将1.5G的文件上传到服务端, 由服务端进行存储,之后提供给其他设备下载。能够支持在网络条件不好时实现 断点续
01
人人都能手写的chrome插件,帮我省了1000多块钱
在网购的世界里,价格波动常常让人感到无奈。《京东价保》插件通过定时监控已购商品价格变化,降价自动申请京东价格保护,帮我省下了不少钱。作为一个前端开发工程师,这让我意识到,手写一个浏览器插件是一件很有趣且有意义的事。于是,我决定尝试自己动手,开发一个简单的二维码生成器插件,各位小伙伴也可参考以下步骤实现自己想要的插件。一、 为什么要手写浏览器插件手写插件有许多好处,以下是一些详细的原因:1.1 个性
01
【黄金圆环】在研发领域的实践分享
这是我参与创作者计划的第1篇文章一、引言在前端开发中,构建工具的选择和使用至关重要。Webpack 一直是前端构建工具的主流选择,但随着前端技术的发展,Vite 作为一种新兴的构建工具,以其快速的开发体验和现代化特性,逐渐受到开发者的青睐。本文将结合黄金圆环法则,详细探讨如何将一个 Webpack 项目迁移到 Vite。通过项目的迁移实践,我们实现了系统项目: 构建时长极大缩短,由原来的120s构
01
不只是前端,后端、产品和测试也需要了解的浏览器知识(二)
继上篇《 不只是前端,后端、产品和测试也需要了解的浏览器知识(一)》介绍了浏览器的基本情况、发展历史以及市场占有率。本篇文章将介绍浏览器基本原理。在掌握基本原理后,通过技术深入,在研发过程中不断创新,推动产品性能、用户体验的提升,来实现业务的增长,创造可持续的价值。一、 业务系统呈现给用户的节点当用户访问我们的业务系统时,浏览器和服务器之间会进行一系列复杂的交互过程。浏览器整体的导航流程如下:以下
jd****
文章数
4
阅读量
84
作者其他文章
01
人人都能手写的chrome插件,帮我省了1000多块钱
01
【黄金圆环】在研发领域的实践分享
01
不只是前端,后端、产品和测试也需要了解的浏览器知识(二)
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号