前言

前段时间我需要实现大文件上传的需求,在网上查找了很多资料,并且也发现已经有很多优秀的博客讲了大文件上传下载这个功能。

我的项目是个比较简单的项目,并没有采用特别复杂的实现方式,所以我这篇文章的目的主要是讲如何最简单地实现大文件上传与下载这个功能,不会讲太多原理之类的东西。

大文件上传

在实际场景中,上传大文件主要会遇到的问题有:

  • 体积大/网络不好时,上传时间会非常久
  • 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
  • Nginx/后端某处对请求大小进行了限制,造成文件因体积过大而上传失败
  • 上传失败后,需要重新开始上传

实现思路

业界最普遍的方案就是切片上传,简单地说就是把文件切割成若干个小文件,再将小文件们传输到后端,最后按照顺序把小文件们重新拼成这个大文件

所以具体的实现逻辑如下:

  1. 把大文件进行切片,对切片的文件内容进行加密生成一个标识串,用于标识唯一的切片

  2. 服务端在临时目录里保存各段文件

  3. 浏览器端所有分片上传完成,发送给服务端一个合并文件的请求

  4. 服务端根据分片顺序进行文件合并

  5. 删除分片文件

也有其他合并文件的方式,本文不做讨论,详情可以参考如何做大文件上传

具体实现

前端部分

前端需要做的部分是:

  • 把大文件进行切片,对切片的文件内容进行加密生成一个标识串
  • 上传所有切片,最后发送合并文件的请求

在这里我使用了一个开源库react-chunk-upload,它提供了加密文件函数和获取文件的相应切片内容的函数(如图),这就不用我自己写啦(偷懒小技巧)。

那么前端部分完整的代码如下:

const [uploadProgress, setUploadProgress] = useState(0);
const [uploadText, setUploadText] = useState(""); const CHUNK_SIZE = 3 * 1024 * 1024; // 设置切片大小为 3Mb
const chunkMD5List = [];
const chunkNum = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < chunkNum; i++) {
const start = i * CHUNK_SIZE; // 切片的开始位置
const end = Math.min(file.size, start + CHUNK_SIZE); // 切片的结束位置
const chunkBlob = blobSlice.call(file, start, end); // 获取相应位置的切片文件
const chunkFile = new File([chunkBlob], "file", {
lastModified: file.lastModified,
});
const md5 = await hashFile(chunkFile, CHUNK_SIZE); // 获取切片标识符
chunkMD5List.push(md5);
await beforeUploadCheckApi(md5) // 上传前检查这个切片是否已存在的接口
.then(async (res) => {
if (res.code === SUCCESS_CODE) {
if (!res.data.exist_status) { // 如果不存在才上传
await uploadChunkCSVApi(chunkFile, md5).then((res) => { // 上传切片的接口
if (res.code === SUCCESS_CODE) {
const progress = Math.floor(((i + 1) / chunkNum) * 10000) / 100; // 计算上传进度,这里为了更好的用户体验,我特意预留了3%给最后的合并文件步骤
setUploadProgress(progress < 3 ? 0 : progress - 3);
}
});
} else {
const progress = Math.floor(((i + 1) / chunkNum) * 10000) / 100;
setUploadProgress(progress < 3 ? 0 : progress - 3);
}
}
})
.catch(() => {
setUploadText("上传失败");
});
}
mergeChunkApi(f.name, JSON.stringify(chunkMD5List)) // 合并切片的接口
.then((res) => {
if (res.code === SUCCESS_CODE) {
setUploadText(`上传 ${file.name} 成功`);
setUploadProgress(100); // 合并文件需要一些时间,所以合并完再让进度条到100
}
})
.catch(() => {
setUploadText(`合并保存文件失败`);
});

后端部分

后端需要提供三个接口,分别是:

  1. 判断切片文件是否已经上传过
  2. 上传切片文件
  3. 合并切片文件

前两个接口的逻辑都很简单,第一个接口是判断文件目录是否存在,第二个接口是把文件放到指定目录

第三个接口的合并逻辑也不难,就是按照顺序读取切片文件然后写入,代码如下:

// 创建一个空文件
filePath := ".....省略"
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println("打开文件失败: %v", err)
} chunkMD5Array := []string{}
```
前端需要传给后端一个切片名称的有序数组,此处省略具体处理过程
``` for _, chunkMD5 := range chunkMD5Array {
chunkPath := fmt.Sprintf("/temp/%v", chunkMD5)
chunk, err := os.Open(chunkPath)
if err != nil {
fmt.Println("打开文件的切片 %v 内容失败: %v", chunkMD5, err)
} content, err := ioutil.ReadAll(chunk)
if err != nil {
fmt.Println("读取文件的切片 %v 内容失败: %v", chunkMD5, err)
} _, err = f.Write(content)
if err != nil {
fmt.Println("写入文件的切片 %v 内容失败: %v", chunkMD5, err)
}
chunk.Close()
} // 写入完毕,关闭文件
f.Close() // 合并后删除切片文件
for _, chunkMD5 := range chunkMD5Array {
chunkPath := fmt.Sprintf("/temp/%v", chunkMD5)
err := os.RemoveAll(chunkPath)
if err != nil {
fmt.Println("删除切片文件%v失败:%v", chunkMD5, err)
}
}

大文件上传就这么简单地搞定了,并且这个实现方法虽然不是断点续传,但是也会大大提高文件的上传速度。

大文件下载

大文件下载的方案则需要区分两种情况:

window.open方法

②分片下载

其余的下载方式,例如a标签下载、表单下载等,都适用于较小文件,这里不讨论。

window.open方法

使用window.open方法有一个前提条件:后端接口返回的是文件流。那么用window.open去开启一个新窗口打开这个链接,浏览器就会去处理下载的过程。前端的示例代码如下:

window.open('http://xxxxxxxxxx', '_blank')

需要注意的地方是后端接口需要指定请求的Content-Disposition属性

在常规的HTTP应答中,Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地——来源 MDN(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition)

优点

  1. 浏览器自己处理下载过程,不需要额外实现进度条等逻辑。
  2. 代码简单。

缺点

  1. 会受到浏览器的兼容性以及浏览器安全策略等因素的影响。
  2. 有时候window.open不会下载文件,而会预览文件,行为不符合预期。
  3. 会新打开一个页面,有些开发者不喜欢这个行为。

分片下载

实现思路

分片下载的逻辑类似于上文所提到的切片上传,具体的实现逻辑如下:

  1. 获取文件的大小
  2. 计算文件的分片数(即需要发送多少次下载分片的请求)
  3. 下载所有分片
  4. 按照顺序合并所有分片
  5. 保存合并好的文件

前端部分

前端代码按照实现思路来讲,可以实现为四个函数:

  • 获取要下载的文件大小
  • 下载文件指定位置的分片blob
  • 合并所有分片blob
  • 保存blob为文件

在这里,我把这个流程封装为了一个开源库react-chunks-to-file,提供后端的接口地址即可完成下载操作。

示例代码:

// 进度
const [percent, setPercent] = useState<number>();
// 状态
const [status, setStatus] = useState<number>(); return(
<ChunksDownload
reqSetting={{
getSizeAPI: `${APP_DOMAIN}/csv/size?`, // 获取文件大小的接口url
getSizeParams: {
token: getToken(),
id: csvId,
},
chunkDownloadAPI: `${APP_DOMAIN}/csv/download_chunk?`, // 下载分片文件的接口url
chunkDownloadParams: {
token: getToken(),
id: csvId,
},
}}
fileName={csv.csv_name}
mime={"text/csv"} // 文件类型
size={3} // 分片大小
concurrency={5} // 并发数
setStatus={setStatus}
setPercent={setPercent}
style={{ display: "inline" }}
>
<Button
type="link"
onClick={() => downloadCSV(csv.csv_name)}
>
下载
</Button>
</ChunksDownload>
);

缺点

  1. 由于使用了blob,不同浏览器对可以下载的文件大小有限制,比如Chrome里是2GB
  2. 使用这个开源库,后端接口的定义需要符合要求,详情请看react-chunks-to-file介绍

优点

  1. 使用简单
  2. 可以自己定义控制下载进度条等其他交互UI,不会新打开窗口
  3. 实现了并发下载

参考资源

如何做大文件上传

JavaScript 中如何实现大文件并行下载?

一文带你层层解锁「文件下载」的奥秘

全网最简单的大文件上传与下载代码实现(React+Go)的更多相关文章

  1. Nginx集群之WCF大文件上传及下载(支持6G传输)

    目录 1       大概思路... 1 2       Nginx集群之WCF大文件上传及下载... 1 3       BasicHttpBinding相关配置解析... 2 4       编写 ...

  2. java实现大文件上传和下载

    [文件上传和下载]是很多系统必备功能, 比如PM\OA\ERP等:系统中常见的开发模式有B/S和C/S,而前者主要是通过浏览器来访问web服务器,一般采用七层协议中的[应用层http]进行数据传输,后 ...

  3. PHP实现大文件上传和下载

    一提到大文件上传,首先想到的是啥??? 没错,就是修改php.ini文件里的上传限制,那就是upload_max_filesize.修改成合适参数我们就可以进行愉快的上传文件了.当然啦,这是一般情况下 ...

  4. ASP.NET实现大文件上传和下载

    总结一下大文件分片上传和断点续传的问题.因为文件过大(比如1G以上),必须要考虑上传过程网络中断的情况.http的网络请求中本身就已经具备了分片上传功能,当传输的文件比较大时,http协议自动会将文件 ...

  5. JSP实现大文件上传和下载

    javaweb上传文件 上传文件的jsp中的部分 上传文件同样可以使用form表单向后端发请求,也可以使用 ajax向后端发请求 1.通过form表单向后端发送请求 <form id=" ...

  6. WEB实现大文件上传和下载

    我们平时经常做的是上传文件,上传文件夹与上传文件类似,但也有一些不同之处,这次做了上传文件夹就记录下以备后用. 这次项目的需求: 支持大文件的上传和续传,要求续传支持所有浏览器,包括ie6,ie7,i ...

  7. xshell简单配置(文件上传和下载)

    1.安装lrzsz 1.1直接安装#yum install lrzsz 1.2sudo命令安装#sudo yum install lrzsz -y检查是否安装成功.#rpm -qa |grep lrz ...

  8. ASP.NET 大文件上传的简单处理

    在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...

  9. ASP.NET 中对大文件上传的简单处理

    在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...

随机推荐

  1. UiPath官网认证中文教程

    RPA之家公众号:RPA之家 RPA之家官网:http://rpazj.com 斗鱼直播:http://www.douyu.com/rpazj UiPath中文社区QQ群:465630324 RPA& ...

  2. Burnside 引理与 Pólya 定理

    群 群的定义 在数学中,群是由一种集合以及一个二元运算所组成的,符合"群公理"的代数结构. 一个群是一个集合 \(G\) 加上对 \(G\) 的二元运算.二元运算用 \(\cdot ...

  3. APISpace 绕口令API接口 免费好用

    绕口令又称急口令.吃口令.拗口令等.是一种民间传统的语言游戏 ,由于它是将若干双声.叠韵词或发音相同.相近的语.词有意集中在一起,组成简单.有趣的语韵,要求快速念出,所以读起来使人感到节奏感强,妙趣横 ...

  4. redis集群的三种方式

    Redis三种集群方式:主从复制,哨兵模式,Cluster集群. 主从复制 基本原理 当新建立一个从服务器时,从服务器将向主服务器发送SYNC命令,接收到SYNC命令后的主服务器会进行一次BGSAVE ...

  5. ROS机械臂 Movelt 学习笔记3 | kinect360相机(v1)相关配置

    目标是做一个机械臂视觉抓取的demo,在基地里翻箱倒柜,没有找到学长所说的 d435,倒是找到了一个老古董 kinect 360. 前几天就已经在旧电脑上配置好了,现在记录在新电脑上的配置过程. 1. ...

  6. 基于cornerstone.js的dicom医学影像查看浏览功能

    最近由于项目需求,需要医学影像.dcm文件的预览功能,功能完成后,基于原生Demo做一个开源分享. 心急的小伙伴可以先看如下基于原生js的全部代码: 一.全部代码 <!DOCTYPE html& ...

  7. Ngnix初步学习

    Nginx下载与安装(Linux) nginx下载 1.root用户下进入/usr/local/src su root cd /usr/local/src 2.下载nginx所需包 # nginx w ...

  8. 什么是双网口以太网IO模块

    MXXXE系列远程IO模块工业级设计,适用于工业物联网和自动化控制系统,MxxxE工业以太网远程 I/O 配备 2 个mac层数据交换芯片的以太网端口,允许数据通过可扩展的菊花链以太网远程 I/O 阵 ...

  9. 转一篇MYSQL文章《数据库表设计,没有最好只有最适合》

    http://mp.weixin.qq.com/s/a8klpzM5iam0_JYSw7-U4g 我们在设计数据库的时候,是否会突破常规,找到最适合自己需求的设计方案,下面来举个例子: 常用的邻接表设 ...

  10. SQL 字符串去除空格函数汇总

    SQL 中使用ltrim()去除左边空格 ,rtrim()去除右边空格 ,没有同时去除左右空格的函数,要去除所有空格可以用replace(字符串,' ',''),将字符串里的空格替换为空 . 例:去除 ...