文件下载解决方案汇总

766 阅读5分钟

关联文章:

《二次封装 Axios 下载文件和上传文件》

《大文件分片并发下载》

之前已经写过很多关于文件下载上传的文章了,但是并没有做汇总,总觉得知识是零散的,不够系统,今天记明天忘!

所以浸提那正儿八经的对文件下载,做个总结!文章分为两部分:传统文件下载和加强版文件下载

一.传统文件下载

1、window.open(url) & window.location.href=url

你打开的只要是一个文件,浏览器会根据 Content-Type 得到文件类型,然后根据判断Content-Disposition决定是下载文件还是预览文件,这种下载的方式简单粗暴,只发送get请求,不能发送post请求。

Content-TypeContent-DispositionHTTP 响应头,他俩共同决定了用户是下载文件还是预览文件。‌也就是说这都是后端接口设置的,与前端无关。

Content-Disposition: inline:‌消息体会以页面的一部分或者整个页面的形式展示,‌用于预览。‌ Content-Disposition:attachment:‌消息体应该被下载,‌默认文件名和URL格式有关。‌如果需要指定下载的文件名,‌可以使用**attachment; filename="filename.jpg"**的形式,‌这样浏览器会呈现一个“保存为”的对话框,‌将指定的文件名预填为下载后的文件名。‌

Content-Type用来告诉你的文件类型是什么

image.png

2、a 标签

对于浏览器不能识别的文件类型,会自动转换下载模式,否则默认自动打开文件,可以手动添加 download 属性,启用下载模式。

a标签下面也有两种方式,一种是明下载,一种是暗下载。

明下载:就是直接写个html标签,然后把href指向接口地址,只要用户点击就会去下载,这种方式的好处是简单粗暴,和window下载一样,都不能扩展,但易于维护。但是相比于window下载,a标签下载只支持下载,不支持预览,不能携带 header 所以不支持添加请求表头的形式来鉴权。

<a href="/url.png" download="filename"></a>

暗下载 就是在js代码里面,写个a标签,等下载结束以后就会把a标签给干掉,这样做的好处就是,在页面上,这个下载功能可以玩出花来,

const a = document.createElement('a')
a.href = 'url'
a.download = 'filename'
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
a.style.display = 'none'
document.body.appendChild(a)
a.click()
a.remove()

3.post请求

这个就是大文件下载的前身,把文件用二进制的方式拿来以后,存在电脑内存里面以后,再用 window.URL.createObjectURL(blob)api,拿到文件在内存里面的url,然后再创建a标签,下载文件,当然你也可以在拿到url以后,预览文件。

它最大的特点就是:支持post请求,可以给通过请求头发送各种参数,支持修改文件名,支持跨域,但是如果是大文件,下载速度会很慢。因为他要处理两次。

axios.post({
    url,
    responseType: 'blob',
    headers: {
      Authorization: getToken()
    }
  }).then((res) => {
    if (res && res.status === 200 && res.data) {
      const { data, headers } = res
      // 从文件中获取文件名称
      let fileName
      if (headers['content-disposition']) {
        fileName = headers['content-disposition'].split(';')[1].split('=')[1]
      } else {
        fileName = data.fileName
      }
      const blob = new Blob([data], { type: headers['content-type'] })
      const downUrl = window.URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = downUrl
      a.download = fileName
      a.style.display = 'none'
      document.body.appendChild(a)
      a.click()
      a.remove()
    }
  })

你会发现axios封装文件下载最核心的代码就是这些。参照《 二次封装 Axios 下载文件和上传文件》

image.png

二.加强版文件下载

大文件下载的方式原理可以参考文章:

juejin.cn/post/738180…

不管你用什么包,它最核心的方式都是分块下载,就是把一个大文件,切割成一个个的小方块,前端会拿到很多二进制切片,然后按照一定的方式,把他们合并成一个完整的文件。

需求场景

用户要在页面上下载3Gzip包?咋办?你还会选择a标签window.open吗?

1. jszip + file-saver

jszip:github.com/Stuk/jszip
file-saver:github.com/eligrey/Fil…

安装工具

npm install jszip file-saver -S

代码示例

import JSZip from 'jszip';
import { saveAs } from 'file-saver';

(async() => {
  // 初始化一个zip打包对象
  const zip = new JSZip();

  // 创建一个名为images的新的文件目录
  const folder = zip.folder('images');

  // 请求远程资源的blob,其他文件,比如视频、表格等也是一样的
  const blob = await fetch('https://abc.com/test.png').then(response => response.blob())

  // 文件夹添加资源,图片也支持base64类型 {base64: true}
  folder.file('test.png', blob, { Blob: true });

  // 把打包内容异步转成blob二进制格式
  zip.generateAsync({type:"blob"}).then((content) => {
    // 下载压缩包
    saveAs(content, 'example.zip');
  });
})()

几行代码轻轻松松下载大文件,并且帮我们进行打包,然后下载他们。这么简单的工具为什么不畅销?因为你用的时候就会发现,当文件太大,他就会卡死不动了。

image.png

看着这个图,我心里飘过一万个真烦人。

2. streamSaver

看到上面的方案,如果您的用户群全部用的是chrome浏览器,那么filesaver完全可以胜任你的工作,如果你的用户群浏览器丰富,就记得要选择streamSaver,虽然streamSaver用起来比较麻烦些,但是这是相对于fileSaver而言的,相比于自己写分块下载来,你就舒服很多了,是不是?

文档地址

mitm.html:github.com/jimmywartin…

sw.js:github.com/jimmywartin…

zip-stream.js:github.com/jimmywartin…

安装工具

npm i streamsaver

代码示例

不打包,只下载,这样用:

import streamSaver from 'streamsaver'

function download(url: string, filename = '') {
  fetch(url, {
    method: 'get',
    headers: {
      Authorization: getToken() as string
    }
  }).then((res) => {
    if (res.status !== 200) {
      ElMessage.error('文件错误,下载失败')
      return
    }
    const readableStream = res.body
    const fileStream = streamSaver.createWriteStream(filename, {
      size: Number(res.headers.get('content-length'))
    })
    if (window.WritableStream && readableStream?.pipeTo) {
      return readableStream?.pipeTo(fileStream).then((res) => {
        console.log('done writeen')
      })
    }
    window.writer = fileStream.getWriter()
    const reader = res.body?.getReader()
    const pump = () =>
      reader
        ?.read()
        .then((res) =>
          res.done
            ? window.writer.close()
            : window.reader.write(res.value).then(pump)
        )
    pump()
  })
  return
}

又打包,又下载,这样用

import streamSaver from 'streamsaver';
// gihub上下载的zip-stream.js
import '@/libs/streamsaver/zip-stream';

// mitm.html的存放目录,sw.js同级存放,都需要跟随项目部署到服务器
streamSaver.mitm = `${location.origin}/libs/streamsaver/mitm.html`;

// 初始化打包对象
const readableZipStream = new window.ZIP({
    async pull(ctrl) {
        // 请求资源 & 设置目录
        const res = await fetch('https://abc.com/test.png');
        const stream = () => res.body;
        const name = '/images/test.png';

        // 添加到处理队列,可遍历添加
        ctrl.enqueue({ name, stream });

        ctrl.close();
    },
});

// 资源命名
const fileStream = streamSaver.createWriteStream('example.zip');

// more optimized
if (window.WritableStream && readableZipStream.pipeTo) {
  return readableZipStream
    .pipeTo(fileStream)
    .then(() => {
      console.log('下载成功')
    })
    .catch((error) => {
      console.error('下载失败', error);
    })
}

// less optimized
const writer = fileStream.getWriter();
const reader = readableZipStream.getReader();
const pump = () =>
  reader
    .read()
    .then(() => {
      console.log('下载成功')
    })
    .catch((error) => {
      console.error('下载失败', error);
    })

pump();

经过简单的测试发现,streamSaver 的兼容性更佳,在一些非谷歌浏览器上,也能达到 2GB体积的下载,所以也是目前实现需求的首选方案。