全网最简单的大文件上传与下载代码实现(React+Go)
前言
前段时间我需要实现大文件上传的需求,在网上查找了很多资料,并且也发现已经有很多优秀的博客讲了大文件上传下载这个功能。
我的项目是个比较简单的项目,并没有采用特别复杂的实现方式,所以我这篇文章的目的主要是讲如何最简单地实现大文件上传与下载这个功能,不会讲太多原理之类的东西。
大文件上传
在实际场景中,上传大文件主要会遇到的问题有:
- 体积大/网络不好时,上传时间会非常久
- 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
- Nginx/后端某处对请求大小进行了限制,造成文件因体积过大而上传失败
- 上传失败后,需要重新开始上传
实现思路
业界最普遍的方案就是切片上传,简单地说就是把文件切割成若干个小文件,再将小文件们传输到后端,最后按照顺序把小文件们重新拼成这个大文件。
所以具体的实现逻辑如下:
把大文件进行切片,对切片的文件内容进行加密生成一个标识串,用于标识唯一的切片
服务端在临时目录里保存各段文件
浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
服务端根据分片顺序进行文件合并
删除分片文件
也有其他合并文件的方式,本文不做讨论,详情可以参考如何做大文件上传。
具体实现
前端部分
前端需要做的部分是:
- 把大文件进行切片,对切片的文件内容进行加密生成一个标识串
- 上传所有切片,最后发送合并文件的请求
在这里我使用了一个开源库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(`合并保存文件失败`);
});
后端部分
后端需要提供三个接口,分别是:
- 判断切片文件是否已经上传过
- 上传切片文件
- 合并切片文件
前两个接口的逻辑都很简单,第一个接口是判断文件目录是否存在,第二个接口是把文件放到指定目录。
第三个接口的合并逻辑也不难,就是按照顺序读取切片文件然后写入,代码如下:
// 创建一个空文件
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)
优点
- 浏览器自己处理下载过程,不需要额外实现进度条等逻辑。
- 代码简单。
缺点
- 会受到浏览器的兼容性以及浏览器安全策略等因素的影响。
- 有时候
window.open
不会下载文件,而会预览文件,行为不符合预期。 - 会新打开一个页面,有些开发者不喜欢这个行为。
分片下载
实现思路
分片下载的逻辑类似于上文所提到的切片上传,具体的实现逻辑如下:
- 获取文件的大小
- 计算文件的分片数(即需要发送多少次下载分片的请求)
- 下载所有分片
- 按照顺序合并所有分片
- 保存合并好的文件
前端部分
前端代码按照实现思路来讲,可以实现为四个函数:
- 获取要下载的文件大小
- 下载文件指定位置的分片
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>
);
缺点
- 由于使用了
blob
,不同浏览器对可以下载的文件大小有限制,比如Chrome里是2GB - 使用这个开源库,后端接口的定义需要符合要求,详情请看
react-chunks-to-file
介绍
优点
- 使用简单
- 可以自己定义控制下载进度条等其他交互UI,不会新打开窗口
- 实现了并发下载
参考资源
全网最简单的大文件上传与下载代码实现(React+Go)的更多相关文章
- Nginx集群之WCF大文件上传及下载(支持6G传输)
目录 1 大概思路... 1 2 Nginx集群之WCF大文件上传及下载... 1 3 BasicHttpBinding相关配置解析... 2 4 编写 ...
- java实现大文件上传和下载
[文件上传和下载]是很多系统必备功能, 比如PM\OA\ERP等:系统中常见的开发模式有B/S和C/S,而前者主要是通过浏览器来访问web服务器,一般采用七层协议中的[应用层http]进行数据传输,后 ...
- PHP实现大文件上传和下载
一提到大文件上传,首先想到的是啥??? 没错,就是修改php.ini文件里的上传限制,那就是upload_max_filesize.修改成合适参数我们就可以进行愉快的上传文件了.当然啦,这是一般情况下 ...
- ASP.NET实现大文件上传和下载
总结一下大文件分片上传和断点续传的问题.因为文件过大(比如1G以上),必须要考虑上传过程网络中断的情况.http的网络请求中本身就已经具备了分片上传功能,当传输的文件比较大时,http协议自动会将文件 ...
- JSP实现大文件上传和下载
javaweb上传文件 上传文件的jsp中的部分 上传文件同样可以使用form表单向后端发请求,也可以使用 ajax向后端发请求 1.通过form表单向后端发送请求 <form id=" ...
- WEB实现大文件上传和下载
我们平时经常做的是上传文件,上传文件夹与上传文件类似,但也有一些不同之处,这次做了上传文件夹就记录下以备后用. 这次项目的需求: 支持大文件的上传和续传,要求续传支持所有浏览器,包括ie6,ie7,i ...
- xshell简单配置(文件上传和下载)
1.安装lrzsz 1.1直接安装#yum install lrzsz 1.2sudo命令安装#sudo yum install lrzsz -y检查是否安装成功.#rpm -qa |grep lrz ...
- ASP.NET 大文件上传的简单处理
在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...
- ASP.NET 中对大文件上传的简单处理
在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...
随机推荐
- Linux shell 2>&1的意思
在脚本里经常看到 ./xxx.sh > /dev/null 2>&1 ./xxx.sh > log.file 2>&1 在shell中输入输出都有对应的文件描述 ...
- Linux shell环境的配置
shell配置文件分类 按生效范围分类:全局和局部 按登录方式分类:交互式和非交互式 按功能分类:profile和bashrc shell配置文件按生效范围分类: 全局配置:针对有所用户有效 /etc ...
- sql-DQL-多表联查
多表查询 笛卡尔积 左表的每条数据和右表的每条数据组合,这种效果称为笛卡尔乘积 select * from emp, dept; 笛卡尔积引入了很多无用的数据,要完成多表查询,需要设置过滤条件来消除无 ...
- P1189 SEARCH—搜索
将这题加进来的原因 因为他的优化令人眼前一新! 题目传送门() 相似的题目之 血色先锋队 ↑这一题也要用到标记数组 优化!!! 对于一个位置, 如果他在同样的深度再一次被访问,那他接下来所走的路径,所 ...
- 静态static关键字概述和静态static关键字修饰成员变量
static关键字 概述 关于 static 关键字的使用,它可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的.也就是说,既然属于类,就可以不靠创建对象来调用了 ...
- VBA驱动SAP GUI完成界面元素值初始化
小爬日常利用VBA完成SAP GUI自动化时,经常被这个问题困扰:我们进入一个事务代码界面时,如FBL1N(供应商行项目显示),很多的 GuiTextField(文本框)对象.GuiCheckBox( ...
- 使用Java客户端发送消息和消费的应用
体验链接:https://developer.aliyun.com/adc/scenario/fb1b72ee956a4068a95228066c3a40d6 实验简介 本教程将Demo演示使用jav ...
- 外贸ERP系统哪些模块比较重要?得具备什么功能?
我国的外贸企业众多,涉及到多个行业,受疫情的影响,部分企业面临着极大的发展难题.而想要更好的在市场当中生存,除了要有更敏锐的市场嗅觉,也要有更大胆的创新.在外贸ERP系统的发展之下,会得到更多企业的青 ...
- mybatis-plus时间字段自动填充
时间代码自动填充的2种方式 数据库方式 将数据库字段create_time和update_time设置CURRENT_TIMESTAMP,create_time字段后面不需要勾选更新,update_t ...
- websocket、socket、http对比
简介 在之前的理解中,讲述了socket.websocket等相关的理解,本文就socket.websocket.http理解一下其对应的联系和区别. HTTP 协议 http 为短连接:客户端发送请 ...