前言

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

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

大文件上传

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

  • 体积大/网络不好时,上传时间会非常久
  • 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
  • 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. 「快速学习系列」我熬夜整理了Vue3.x响应性API

    前言 Vue3.x正式版发布已经快半年了,相信大家也多多少少也用Vue3.x开发过项目.那么,我们今天就整理下Vue3.x中的响应性API.响应性APIreactive 作用: 创建一个响应式数据. ...

  2. MES 系统介绍

    MES系统是一套面向制造企业车间执行层的生产信息化管理系统.MES可以为企业提供包括制造数据管理.计划排程管理.生产调度管理.库存管理.质量管理.人力资源管理.工作中心/设备管理.工具工装管理.采购管 ...

  3. SAP BDC 用户输入日期转系统日期格式: CONVERT_DATE_TO_EXTERNAL

    BDC中,日期输入格式不正确:可调用FM  CONVERT_DATE_TO_EXTERNAL DATA:l_bdcfield LIKE bdcdata-fval."BDC field val ...

  4. WPF开发随笔收录-带递增递减按钮TextBox

    一.前言 今天分享一下如何实现带递增递减按钮的TextBox控件 二.正文 1.之前的博客分享了一篇自定义XamlIcon控件的文章,这次就直接在那个项目的基础上实现今天这个自定义控件 2.首先添加两 ...

  5. netty系列之:kequeue传输协议详解

    目录 简介 KQueueEventLoopGroup KQueueEventLoop KQueueServerSocketChannel和KQueueSocketChannel 总结 简介 在前面的章 ...

  6. MarkDown语法——更好地写博客

    MarkDown语法--更好地写博客 我们在学习过程中要尽量养成编写博客的 好习惯:一方面方便自己在学习之后进行一次汇总,其次自己书写的文章可以在以后的时间里反复查看以便于巩固,在找工作时博客也是被招 ...

  7. vue 项目知识

    Vue使用 Vue 源码解析 Vue SSR 如何调试Vue 源码 如何学习开源框架---> 从它的第一次commit 开始看 国外的文章 大致了解写框架的过程(英文关键字) 找到关键---&g ...

  8. java-数据输入,分支结构

    数据输入 1.Scanner使用的基本步骤" 导包:import java.util.Scanner;(导包的动作必须出现在类定义的上边) 创建对象:Scanner sc = new Sca ...

  9. 一文吃透如何部署kubernetes之Dashboard

    kubernetes Dashboard是什么? Dashboard是kubernetes的Web GUI,可用于在kubernetes集群上部署容器化应用,应用排错,管理集群本身及其附加的资源等,它 ...

  10. 创建私有CA,我就用openSSL

    目录 简介 搭建root CA 生成root CA 使用CRL 使用OSCP 总结 简介 一般情况下我们使用的证书都是由第三方权威机构来颁发的,如果我们有一个新的https网站,我们需要申请一个世界范 ...