前言

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

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

大文件上传

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

  • 体积大/网络不好时,上传时间会非常久
  • 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
  • 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. linux安装源码包指定安装目录

    当下载完一个源码包并且解压后 文件夹下会有一个重要的文件configure configure 文件是一个可执行的脚本文件,它将检查目标系统的配置和可用功能,比如一些检查依赖或者启用禁用一些模块,它有 ...

  2. 深入解析kubernetes中的选举机制

    Overview 在 Kubernetes的 kube-controller-manager , kube-scheduler, 以及使用 Operator 的底层实现 controller-rumt ...

  3. python小题目练习(十一)

    题目:大乐透号码生成器 需求:使用Random模块模拟大乐透号码生成器,选号规则为:前区在1 ~ 35的范围内随机产生不重复 的5个号码,后区在1~ 12的范围内随机产生不重复的2个号码.效果如图8. ...

  4. 在Visual Studio Code 中配置Python 中文乱码问题

    在Visual Studio Code 中配置Python 中文乱码问题 方法一:直接代码修改字符集 添加前四行代码 import io import sys #改变标准输出的默认编码 sys.std ...

  5. tauri+vue开发小巧的跨OS桌面应用-股票体检

    最近打算写一个用于股票体检的软件,比如股权质押比过高的股票不合格,ROE小于10的股票不合格,PE大于80的股票不合格等等等等,就像给人做体检一样给股票做个体检.也实现了一些按照技术指标.基本面自动选 ...

  6. Codeforces Round #780 (Div. 3)

    A. Vasya and Coins 题目链接 题目大意 Vasya 有 a 个 1-burle coin,有 b 个 2-burle coin,问他不能通过不找钱支付的价格的最小值. 思路 如果 a ...

  7. (零)机器学习入门与经典算法之numpy的基本操作

    1.根据索引来获取元素* 创建一个索引列表ind,用来装载索引,当numpy数据是一维数据时:一个索引对应的是一个元素具体的例子如下: import numpy as np # 数据是一维数据时:索引 ...

  8. 深入理解 Java 对象的内存布局

    对于 Java 虚拟机,我们都知道其内存区域划分成:堆.方法区.虚拟机栈等区域.但一个对象在 Java 虚拟机中是怎样存储的,相信很少人会比较清楚地了解.Java 对象在 JVM 中的内存布局,是我们 ...

  9. XML入门介绍

    目录 XML 简介 xml 语法 文档声明 (1)创建一个 xml 文件 (2)图书有 id 性 属性 一 表示唯一 标识,书名,有作者,价格的信息 xml 注释 元素(标签) 1)什么是 xml 元 ...

  10. C++类中的常成员和静态成员

    常变量.常对象.常引用.指向常对象或常变量的指针等在定义时都使用了const关键字,这是C++语言引入的一种数据保护机制,称为const数据保护机制.例如通过const关键字主动地将被调函数形参进行限 ...