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

   2023-02-08 学习力0
核心提示:前言前段时间我需要实现大文件上传的需求,在网上查找了很多资料,并且也发现已经有很多优秀的博客讲了大文件上传下载这个功能。我的项目是个比较简单的项目,并没有采用特别复杂的实现方式,所以我这篇文章的目的主要是讲如何最简单地实现大文件上传与下载这

前言

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

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

大文件上传

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

  • 体积大/网络不好时,上传时间会非常久
  • 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
  • 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 中如何实现大文件并行下载?

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

 
标签: 前端
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • React前端框架路由跳转,前端回车事件、禁止空
    react router - historyhistory.push() 方法用于在JS中实现页面跳转history.go(-1) 用来实现页面的前进(1)和后退(-1)访问js连接后+?v1清缓存标签中的Class=“a b c”a b c分别代表3个样式名称!分别用空格隔开!Struts2》ajax请求后台,后台转换json返回aja
    03-08
  • 基于ZR.VUE 前端的改造,页面刷新报错
     问题描述:前后端分离开发,分开部署. 页面刷新 直接报404 错误的解决办法提示:  先在 .env.development 中 配置 VUE_APP_BASE_API , 将 '/' 替换为 后端地址 'http://localhost:8888/'如果是对应的发布的正式环境,也要修改  .env.production 的VUE_APP_
    03-08
  • Vue.js 前端项目在常见 Web 服务器上的部署配置
    Vue.js 前端项目在常见 Web 服务器上的部署配置
    Vue.js 前端项目在 Web 服务器上的部署配置Web 服务器是一种用于存储,处理和传输Web内容的软件。它是一种特殊类型的服务器,具有处理 HTTP 请求并向浏览器返回 Web 页面和其他内容的能力。Web服务器支持多种编程语言,如 PHP,JavaScript,Ruby,Python 等,
    03-08
  • 基于Vue3实现前端埋点上报插件并打包发布到npm的详细过程
    基于Vue3实现前端埋点上报插件并打包发布到npm
    目录项目环境搭建插件开发点击事件上报vue自定义指令手动上报方法页面访问次数上报(pv,uv)页面停留时间(TP)获取公共参数引入axios打包发布使用说明OptionOptions 示例点击指令上报手动上报写在最后前端埋点对于那些营销活动的项目是必须的,它可以反应出
    03-08
  • javascript前端如何使用google-protobuf
    1.首先下载google的protobuf的compiler,通过编译器可以将.proto文件转换为想要的语言文件。下载地址:https://repo1.maven.org/maven2/com/google/protobuf/protoc/2.写一个proto文件syntax = "proto3";message messagebody{//工厂 3Gstring factory = 1;//
    03-08
  • 前端笔记之JavaScript(六)让人头疼的正则表达式
    前端笔记之JavaScript(六)让人头疼的正则表达
    1.1正则概述和体验正则表达式是被用来匹配字符串中的字符组合的模式,常用来做表单验证。在JavaScript中,正则表达式也是对象,是一种引用类型。案例:正确输入一个电话号码,010-12345678,用户输入正确返回“对”,错误返回“错” var tel = prompt("请输
    03-08
  • 前端笔记之JavaScript(八)关于元素&计算后的样式
    前端笔记之JavaScript(八)关于元素&计算后的
    1.1概述得到id元素的方法 document.getElementById()得到一个元素。事实上,还有一个方法可以得到标签元素,并且得到的是多个元素: document.getElementsByTagName(); 全线浏览器兼容的,得到元素的方法,就这两个:document.getElementById()      
    03-08
  • 前端优化分析 之 javascript引用位置优化
    在很多优化法则中都提到,尽量将javascript放到页面底部,这是为什么呢我通过firebug进行了下简单的分析 看下图  本页面首尾都存在javascript代码 我们分析得出 1、整个页面文档家在结束才开始加载css和js以及其他的数据 2、当顶部的所有js都家在结束之
    03-08
  • 500行JavaScript代码在前端根据数据生成CAD工程
    前言 用数据生成CAD图,一般采用的ObjectArx对CAD二次开发完成。ObjectARX是AutoDesk公司针对AutoCAD平台上的二次开发而推出的一个开发软件包,它提供了以C++为基础的面向对象的开发环境及应用程序接口,能访问和创建AutoCAD图形数据库。而由于现在懂C++的人
    03-08
  • 学习前端页面css定位 网页前端设计css
    一、相对定位:absolute  相对定位是一个非常容易掌握的概念。如果对一个元素进行相对定位,它将出现在它所在的位置上。然后,可以通过设置垂直或水平位置,让这个元素“相对于”它的起点进行移动。position:relative;同时可以设置上下左右位置偏移;rela
    03-08
点击排行