blog
blog copied to clipboard
前端实现点击下载全攻略
typora-copy-images-to: ../../../Downloads/node包.jpeg
最新更新 可见 掘金专栏
前端实现点击下载全攻略
最近在做的后台管理系统,来了个小需求~要实现 excel
文件的导出功能。
当时眉头一皱,感觉多年以前似乎写过这样的需求,知道该怎么实现却又模模糊糊
这次正好就来梳理一下吧~
基本概念
1. HTML5 a 标签的download
属性
此属性指示浏览器下载 URL 而不是导航到它,因此将提示用户将其保存为本地文件。如果属性有一个值,那么此值将在下载保存过程中作为预填充的文件名(如果用户需要,仍然可以更改文件名)。此属性对允许的值没有限制,但是
/
和\
会被转换为下划线。大多数文件系统限制了文件名中的标点符号,故此,浏览器将相应地调整建议的文件名。注:
- 此属性仅适用于同源 URL。
- 尽管 HTTP URL 需要位于同一源中,但是可以使用
blob:
URL 和data:
URL,以方便用户下载使用 JavaScript 生成的内容(例如使用在线绘图 Web 应用程序创建的照片)。- 如果 HTTP 头中的 Content-Disposition 属性赋予了一个不同于此属性的文件名,HTTP 头属性优先于此属性。
- 如果 HTTP 头属性
Content-Disposition
被设置为inline(即Content-Disposition='inline'
),那么 Firefox 优先考虑 HTTP 头Content-Disposition
download 属性。
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
是一个包含二进制数据的 JavaScriptArrayBuffer
。"blob"
response
是一个包含二进制数据的Blob
对象 。"document"
response
是一个 HTMLDocument
或 XMLXMLDocument
,这取决于接收到的数据的 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>
然而。。。这玩意儿的兼容性有点问题
还附带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",
▄█▀█●
坑爹呐这是,老子踏马查了一个下午
从怀疑后端数据格式,到怀疑浏览器版本,到怀疑office
和wps
区别,到怀疑编解码格式。。。
最后准备在怀疑人生中郁郁而终的时候
我特么非常幸运 非常凑巧 的点到了我的 package.json
看到了 mockjs
(我是刚接这个项目,以前也没用过d2,反正就一后台项目用的集成框架呗,拿来就用呗,所以我其实一开始还不知道里面有个mockjs
呢)
但是我知道 mockjs
的实现。。就是会动我的 XMLHttpRequest
。 但是我不知道它还有那个bug -,-
然后我特么就灵机一动 就觉着是mock的问题
特么果然是这个批
我敲了。。。
另外,我给d2提了 issue,坐等。。。
PS
写一下另外的一些感想吧。。
关于后台项目的问题。。好多都是用高度集成的框架什么的
其实。。。说实话 我觉着那些框架就是 纯后端 甚至是 产品啊 设计啊 什么的用的
无非就是老板要看看数据,你就整个后台嘛
这种后台。。。还要一个前端去开发的后台。。。
用个p的集成框架啊,个破后台能有多复杂,你一个专业的前端啊,三下两下就搭完了的东西
自己搭建的好处就是 纯净 可控 稳定 , 这不就是一个后台系统的意义么?
这不就是一个后台系统的意义么?
你越依赖其他的库这个系统就越脆弱, 你看似 大而全 实际 空且弱
没有用的库一大堆,没用的功能也一大堆,到处做减法,到处写bug
且不说框架规定的各种配置各种依赖各种命名各种套路,你不但要写业务逻辑,你还得去看框架作者的逻辑
你的那些库万一有bug,万一一升级,万一不兼容了,加班加点的还是你