点击 PDF 链接,如何触发下载而非预览?

1,995 阅读3分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

前言

在一些项目中我们常常使用静态文件服务器,作为一些资源的存储,这样的静态服务器配置简单,使用成本低,访问便捷,常用于图片、视频、PDF 等文件的在线预览。

当我们需要下载这些文件的时候,创建 a 标签,利用 a 标签的点击事件也能够很方便的进行下载。但是,如果这个文件是 PDF,我们在页面上点击一个 PDF 链接的时候,会直接打开新窗口预览文件。

浏览器可以解析的文件,比如.txt,.png,.pdf....浏览器就会采取预览模式,.exe,.xlsx..那么浏览器会自动下载

那么如何做到点击链接使其触发下载事件而不是预览呢?

方案1

通过设置浏览器,使其下载 pdf 而不是在 浏览器中自动打开它们。

步骤:

浏览器设置 -> 高级 -> 内容设置 -> pdf

虽然简单便捷,但是作为一个合格前端开发工程师儿,我们要时刻想着以用户为本,用户就是上帝,我们怎么能麻烦上帝去自己更改设置呢?

方案2

将 PDF 文件的拓展名更改为大写。

如果扩展名是.pdf,则默认会预览。如果扩展名为.PDF,则会直接下载。

下面是同一个文件分别以 .PDF.pdf 命名,点击查看效果。

触发下载:

test-jpfile1.oss-cn-shenzhen.aliyuncs.com//Bom/bom/20…

触发预览:

test-jpfile1.oss-cn-shenzhen.aliyuncs.com//Bom/bom/20…

或者:

<body>
  <button onclick="downloadEvt('https://test-jpfile1.oss-cn-shenzhen.aliyuncs.com//Bom/bom/2022/1/19/2022011911370824626513.pdf')">pdf</button>
  <button onclick="downloadEvt('https://test-jpfile1.oss-cn-shenzhen.aliyuncs.com//Bom/bom/2022/1/19/2022011911355693652034.PDF')">PDF</button>
  <script>
    function downloadEvt(url, fileName = '未知文件') {
      const el = document.createElement('a');
      el.style.display = 'none';
      el.setAttribute('target', '_blank');
      fileName && el.setAttribute('download', fileName);
      el.href = url;
      console.log(el);
      document.body.appendChild(el);
      el.click();
      document.body.removeChild(el);
    }
  </script>
</body>

这种方案存在弊端,如果用户是自己的话,在上传 pdf 文件的时候可以刻意去更改文件的拓展名,但是需要用户上传的话,总不能要求所有用户在上传前更改一遍文件的拓展名吧。

方案3

ajax下载(Blob - 利用Blob对象生成Blob URL)

首先使用 new XMLHttpRequest() 拿到这个文件流,前端获取到服务器端生成的字节流,此时数据是存在于js的内存中的,是不可以直接保存在本地的,利用Blob对象和window.URL.createObjectURL对象生成一个虚拟的URL地址,然后在利用浏览器的特性进行下载。

ajax.responseType = 'blob' 用于指定返回数据的类型,如果不加的话,下载下来的文件可能导致打不开。

function downLoadAjaxEvt(){
  const ajax = new XMLHttpRequest();
  ajax.responseType = 'blob'; 
  
  ajax.onload = function () {
    if (this.status === 200 || this.status === 304) {
      // 通过FileReader去判断接口返回是json还是文件流
      const fileReader = new FileReader();
      fileReader.readAsDataURL(this.response);
      
      fileReader.onload = () => {
        if (this.getResponseHeader('content-type').includes('application/json')) {
          alert('服务器出现问题,请联系管理员');
        } else {
          // decodeURIComponent/decodeURI
          //(主要获取后缀名,否则某些浏览器会一律识别为txt,导致下载下来的都是txt)
          const _fileName = decodeURIComponent((this.getResponseHeader('content-disposition') || '; filename="未知文件"').split(';')[1].trim().slice(9));
          
          // 也可以用FileSaver(需提前引入https://github.com/eligrey/FileSaver.js)
          // saveAs(fileReader.result, _fileName);
          downloadEvt(fileReader.result, _fileName);
        }
      }
    } else {
      alert('服务器出现问题,请联系管理员');
    }
  };
  
  // 发送请求
  ajax.send(queryParams);
}  

function downloadEvt(url, fileName = '未知文件') {
  const link = document.createElement('a');
  link.style.display = 'none';
  link.setAttribute('target', '_blank');
  fileName && el.setAttribute('download', fileName);
  link.href = url;
  document.body.appendChild(el);
  link.click();
  document.body.removeChild(el);
};

方案 3 就很满足需求了

如果需要甚至可以在请求头中加上token鉴权:

ajax.setRequestHeader('Authorization', _config.token);
ajax.setRequestHeader('Content-Type', _config.contentType);

也可以监控文件下载的进度:

// 获取文件下载进度
ajax.addEventListener('progress', (progress) => {
  const percentage = ((progress.loaded / progress.total) * 100).toFixed(2);
  const msg = `下载进度 ${percentage}%...`;
  console.log(msg);
});

参考文章:

常见文件下载方式