blog icon indicating copy to clipboard operation
blog copied to clipboard

前端实现点击下载全攻略

Open YuArtian opened this issue 5 years ago • 0 comments


typora-copy-images-to: ../../../Downloads/node包.jpeg

最新更新 可见 掘金专栏

前端实现点击下载全攻略

最近在做的后台管理系统,来了个小需求~要实现 excel 文件的导出功能。

当时眉头一皱,感觉多年以前似乎写过这样的需求,知道该怎么实现却又模模糊糊

眉头一皱

这次正好就来梳理一下吧~

基本概念

1. HTML5 a 标签的download属性

​ 此属性指示浏览器下载 URL 而不是导航到它,因此将提示用户将其保存为本地文件。如果属性有一个值,那么此值将在下载保存过程中作为预填充的文件名(如果用户需要,仍然可以更改文件名)。此属性对允许的值没有限制,但是 /\ 会被转换为下划线。大多数文件系统限制了文件名中的标点符号,故此,浏览器将相应地调整建议的文件名。

注:

  • 此属性仅适用于同源 URL。
  • 尽管 HTTP URL 需要位于同一源中,但是可以使用 blob: URLdata: URL,以方便用户下载使用 JavaScript 生成的内容(例如使用在线绘图 Web 应用程序创建的照片)。
  • 如果 HTTP 头中的 Content-Disposition 属性赋予了一个不同于此属性的文件名,HTTP 头属性优先于此属性。
  • 如果 HTTP 头属性 Content-Disposition 被设置为inline(即 Content-Disposition='inline'),那么 Firefox 优先考虑 HTTP 头 Content-Dispositiondownload 属性。

2. URL.createObjectURL()

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。

注:在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。

3.HTMLCanvasElement.toDataURL()

HTMLCanvasElement.toDataURL() 方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。

  • 如果画布的高度或宽度是0,那么会返回字符串“data:,”。
  • 如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。
  • Chrome支持“image/webp”类型。

4.指定XMLHttpRequest.response的数据类型

有些时候我们希望xhr.response返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response拿到的直接就是一个 js 对象,我们该怎么实现呢? 有2种方法可以实现,一个是level 1就提供的overrideMimeType()方法,另一个是level 2才提供的xhr.responseType属性

XMLHttpRequest.overrideMimeType

XMLHttpRequest 的 overrideMimeType 方法是指定一个MIME类型用于替代服务器指定的类型,使服务端响应信息中传输的数据按照该指定MIME类型处理。例如强制使流方式处理为"text/xml"类型处理时会被使用到,即使服务器在响应头中并没有这样指定。此方法必须在send方法之前调用方为有效

XMLHttpRequest.responseType

XMLHttpRequest.responseType 属性是一个枚举类型的属性,返回响应数据的类型。它允许我们手动的设置返回数据的类型。如果我们将它设置为一个空字符串,它将使用默认的"text"类型

当将responseType设置为一个特定的类型时,你需要确保服务器所返回的类型和你所设置的返回值类型是兼容的。

那么如果两者类型不兼容呢? 恭喜你,你会发现服务器返回的数据变成了null,即使服务器返回了数据。

还有一个要注意的是,给一个同步请求设置responseType会抛出一个InvalidAccessError 的异常

responseType 常用的几种值

描述
"" responseType 设为空字符串与设置为"text"相同, 是默认类型 (实际上是 DOMString)。
"arraybuffer" response 是一个包含二进制数据的 JavaScript ArrayBuffer
"blob" response 是一个包含二进制数据的 Blob 对象 。
"document" response 是一个 HTML DocumentXML XMLDocument ,这取决于接收到的数据的 MIME 类型。请参阅 HTML in XMLHttpRequest 以了解使用 XHR 获取 HTML 内容的更多信息。
"json" response 是一个 JavaScript 对象。这个对象是通过将接收到的数据类型视为 JSON 解析得到的。
"text" response 是包含在 DOMString 对象中的文本。

虽然在xhr level 2中,2者是共同存在的。但其实不难发现,xhr.responseType就是用来取代xhr.overrideMimeType()的,xhr.responseType功能强大的多,xhr.overrideMimeType()能做到的xhr.responseType都能做到。所以我们现在完全可以摒弃使用xhr.overrideMimeType()

实践

1. 下载一张图片

​ 这个是最简单的 ~

​ 只要一个属性就搞定啦

<a href="demo.png" download>点我下载</a>

​ 想要修改名字也可以,只需要在 download属性里写好

<a href="demo.png" download="我的新名字.png">点我下载</a>

​ 然而。。。这玩意儿的兼容性有点问题

download_兼容性

还附带3个已知的bug

![image-20190616153634100](/Users/yuartian/Library/Application Support/typora-user-images/image-20190616153634100.png)

Emmmmmm 。。。 IE 又是你!

2. 下载动态文件

​ 有时候需要下载实时生成的内容就不能只用HTML标签解决了。我们需要用JS修改 href属性。

  • 下载文本文件
    function(data, filename){
      let a = document.createElement("a");
      let blob = new Blob([data]);
      let url = window.URL.createObjectURL(blob);
      a.href = url;
      a.download = filename;
      a.style.display = "none";
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
    }
    
  • 下载图片文件
    function(img, filename){
      let a = document.createElement("a");
      let canvas = document.createElement('canvas');
      let context = canvas.getContext('2d');
      let width = domImg.naturalWidth;
      let height = domImg.naturalHeight;
      context.drawImage(img, 0, 0);
      a.href = canvas.toDataURL('image/jpeg');
      //a.href = canvas.toDataURL('image/png'); ...
      a.download = filename;
      a.style.display = "none";
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
    }
    

3. 结合异步请求

​ 如果数据是后端返回的话,对于data的处理和上面的一样。filename一般会放在响应头的content-disposition

let contentDisposition = response.headers["content-disposition"];
//从 response 的 headers 中获取 filename
//后端 response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名;
let patt = new RegExp("filename=([^;]+\\.[^\\.;]+);*");
let result = patt.exec(contentDisposition);
let filename = decodeURI(result[1]);

使用 axios 的全部代码

// request.js
import axios from 'axios'
export function getReportExcel(){
  return axios({
    //这一行 非 常 重 要
    //没有正确的 responseType 会导致乱码
    responseType: 'blob',
    method: 'GET',
    url: 'url',
  })
}

// report.vue
import { getReportList } from '@/request'
/* 导出报表 */
    async exportExcel () {
      try {
        let a = document.createElement("a");
        const response = await getReportExcel()
        //从 response 的 headers 中获取 filename
        //后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名;
        let contentDisposition = response.headers["content-disposition"];
        let patt = new RegExp("fileName=([^;]+\\.[^\\.;]+);*");
        let result = patt.exec(contentDisposition);
        let filename = decodeURI(result[1]);
        //数据处理
        let blob = new Blob([response.data], {type: 'application/vnd.ms-excel'});
        let url = window.URL.createObjectURL(blob);
        a.href = url;
        a.download = filename;
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a);
      } catch (error) {
        console.log('error', error);
      }

一定要看的天坑---D2admin中的Mockjs乱码问题

之前公司后台项目都是我一手搭建的,所以从来没有什么 不需要的 或者 一堆乱七八糟,花里胡哨的库

但是现在公司已存的这个后台 使用了 d2-admin

-,-

这篇文章不是吐槽它做的不好啊 虽然它也确实不太行

D2这框架内置了 mockjs ,这本来无可厚非嘛 反正现在都流行内置这么个玩意 显得自己功能全,能提高啥啥开发效率的

mockjs有个bug就是在替换 XMLHttpRequest 的时候会导致 responseType 丢失

所以导出的请求中我们写的 responseType 头失效了,结果就是Excel乱码了

但是这个问题后来在 4.0 中被修复了 相关bug地址

但是 d2 好死不死的用了个低版本的 "mockjs": "^1.0.1-beta3",

▄█▀█●

坑爹呐这是,老子踏马查了一个下午

从怀疑后端数据格式,到怀疑浏览器版本,到怀疑officewps区别,到怀疑编解码格式。。。

最后准备在怀疑人生中郁郁而终的时候

我特么非常幸运 非常凑巧 的点到了我的 package.json 看到了 mockjs

(我是刚接这个项目,以前也没用过d2,反正就一后台项目用的集成框架呗,拿来就用呗,所以我其实一开始还不知道里面有个mockjs呢)

但是我知道 mockjs 的实现。。就是会动我的 XMLHttpRequest 。 但是我不知道它还有那个bug -,-

然后我特么就灵机一动 就觉着是mock的问题

特么果然是这个批

我敲了。。。

另外,我给d2提了 issue,坐等。。。

PS

写一下另外的一些感想吧。。

关于后台项目的问题。。好多都是用高度集成的框架什么的

其实。。。说实话 我觉着那些框架就是 纯后端 甚至是 产品啊 设计啊 什么的用的

无非就是老板要看看数据,你就整个后台嘛

这种后台。。。还要一个前端去开发的后台。。。

用个p的集成框架啊,个破后台能有多复杂,你一个专业的前端啊,三下两下就搭完了的东西

自己搭建的好处就是 纯净 可控 稳定 , 这不就是一个后台系统的意义么?

这不就是一个后台系统的意义么?

你越依赖其他的库这个系统就越脆弱, 你看似 大而全 实际 空且弱

没有用的库一大堆,没用的功能也一大堆,到处做减法,到处写bug

且不说框架规定的各种配置各种依赖各种命名各种套路,你不但要写业务逻辑,你还得去看框架作者的逻辑

你的那些库万一有bug,万一一升级,万一不兼容了,加班加点的还是你

node包

YuArtian avatar Jun 14 '19 02:06 YuArtian