前言
在中后台项目中,前端难免需要处理下载的逻辑,需要下载的内容包括但不限于图片、Excel表格、CSV文件、MP4文件、PDF文件、TXT文件、JSON文件、HTML文件等等。虽然下载的内容各式各样,但是下载的原理大同小异。下面来一起学习一下前端是如何处理下载的。
正文
在讲下载之前,需要了解几个JS的对象,因为这些对象都跟下载息息相关。
前置知识
Blob、File、URL.createObjectURL、URL.revokeObjectURLURL
Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableSteam来用于数据操作。
Blob 表示的不一定是JavaScript原生格式的数据。File接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
Blob 构造函数用法举例:
File对象接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。
通常情况下, File 对象是来自用户在一个 input元素上选择文件后返回的FileList对象,也可以是来自由拖放操作生成的DataTransfer 对象,或者来自HTMLCanvasElement上的 mozGetAsFile() API。
File 对象是特殊类型的Blob,且可以用在任意的 Blob 类型的 context 中。比如说,FileReader, URL.createObjectURL(), createImageBitmap() (en-US), 及 XMLHttpRequest.send() 都能处理 Blob 和 File。
监听Input的change事件可以在FileList数组上获取File对象。
URL.createObjectURL(object) 静态方法会创建一个 DOMString,返回格式类似'blob:http://localhost:4200/0e40281d-92e9-40cf-af54-6193fb3a3f8c'。
它接受一个object参数,用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。返回一个DOMString包含了一个对象URL,该URL可用于指定源 object的内容。
在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。
浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。
在前端处理下载流程中,首选使用URL.createObjectURL处理。
Base64、atob、btoa
Base64是一种用64个字符来表示任意二进制数据的方法。在前端中应用于如使用Base64来展示小图片,减少HTTP请求;某些文件可以避免跨域的问题;还有我们常见的一些二进制文件,如.xlsx、.pdf等。
在 JavaScript 中,很多人不知道的是,其实有两个函数被分别用来处理解码和编码 base64 字符串,atob和btoa方法。
btoa方法用于编码,而atob方法用于解码。但是在某种情况下调用window.btoa会造成Character Out Of Range 的异常。具体可以参考一下MDN(链接在底部给出)。
ArrayBuffer、Unit8Array
ArrayBuffer、Unit8Array(无符号8位整数)是JavaScript用来操作二进制数据的。具体的概念可以参考MDN上的问题,这里就不具体阐述了。之所以在这篇文章中提出来,是因为在downloadjs中需要用到这个对象,大家可以去了解了解。
Blob、File、Base64、ArrayBuffer相互转换
为什么要先前置让大家先了解一下这几个对象呢?因为在前端下载流程中,就是用Blob、相对路径或者Base64来实现下载的。
往往我们获取图片或者其他文件时,却并非是我们想要的格式,那么可以通过以下一些方法来实现相互转换。
File转成ArrayBuffer、Base64:
export function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var reader = new FileReader();
reader.onload = function (e) {
console.log(e.target.result);
};
// 转arrayBuffer
reader.readAsArrayBuffer(file);
// or 转 base64
reader.readAsDataURL(file);
}
}
Base64转成Blob:
export function dataUrlToBlob(strUrl) {
const parts = strUrl.split(/[:;,]/);
const type = parts[1];
const indexDecoder = strUrl.indexOf('charset') > 0 ? 3 : 2;
const decoder = parts[indexDecoder] == 'base64' ? atob : decodeURIComponent;
const binData = decoder(parts.pop());
const mx = binData.length;
const i = 0;
const uiArr = new Uint8Array(mx);
for (i; i < mx; ++i) uiArr[i] = binData.charCodeAt(i);
return new myBlob([uiArr], { type: type });
}
Blob转成Base64
export function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onabort = (e) => reject(e.message);
reader.onerror = (e) => reject(e.message);
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(blob);
});
}
同源(域)和跨域
需要明确的是,单单从前端去处理跨域下载,是不可能的。原因是浏览器同源策略的限制。目前网络上提供的一些方法,如动态创建iframe或者form表单手动触发submit方法去跨域下载,只是允许你在前端发起一个跨域请求,而是否能够下载需要配合后台(CORS和Content-Type、Content-Disposition)来完成。
终于要进入正题了!普天同庆
前端下载(同源)
A标签
处理前端下载,首选的方式就是使用A标签。HTML5针对A标签,有一个download属性。这个属性指示浏览器下载href而不是导航它。这个属性仅支持同源URL。
搭配download属性,href可以设置以下值:
- 同域URL
- blob:
- data:URL
如以下代码:
如果当前的url是跨域的URL,会走下载流程吗?答案是不会,点击之后会直接跳转到该图片上。
使用a标签的download属性来下载时,推荐以下方式
- 优先使用blob + URL方式。
- 其次是Base64方式。在不支持URL的情况下,可以采用这种方式,小文件可以采用这种,大文件不推荐。
- 最后是本地文件方式。不推荐使用这种方式,因为会增加JS打包体积。可以将图片上传到cdn。
那我们要怎么下载一个第三方图片呢,可以看下参考一下前端跨域下载的内容。
window.open
window.open方法主要是在浏览器不支持a标签的download属性中使用,使用方式跟a标签相同。
但是这里需要注意的是,当页面是https,open的url是http的情况下,会导致Chrome浏览器出现Mixed Content的错误。错误大致如下:
如果出现这种情况,可以尝试以下方法来解决:
- 如果请求资源支持https,可以在请求头加上,这样浏览器会将http请求转成https请求,就不会再出现Mixed Content错误了。
-
- 通过后台方式处理。
Ajax下载
可能有人会想,a标签和window.open,都是向浏览器发起了HTTP(S)请求,那我们是不是也可以通过ajax或者fetch来实现下载呢?
答案是不行。可以先来看看结果。当我们通过Ajax向后台发起一个download请求时,请求结果如下:
可以看到,正常情况下,当Response Header存在content-type为application/octet-stream的时候,浏览器会唤起下载框。而通过Ajax发起的请求,浏览器没有唤起下载框,反而是直接将我们需要下载的内容通过Response返回来了。
如果大家有遇到过这个问题,不知道是不是会觉得奇怪呢? 明明都是GET请求,a标签的download可以,Ajax发起GET请求却不行?
Chrome浏览器的多进程架构中,存在一个渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页。出于安全考虑,渲染进程都是运行在沙箱模式下。你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据。
这也就是为什么Ajax无法唤起下载框的原因了。
前端下载(跨域)
由于浏览器同源限制,当你想要实现跨域下载资源时,往往都需要后台的配合。但是实现起来,也相对简单。
在Response中,需要后台设置CORS和两个header
res.header('Content-Type', 'application/octet-stream')
res.header('Content-Disposition', `attachment;filename=${filename}`)
浏览器在识别到其 Content-Type 的值是 application/octet-stream,显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。
如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,即呼起下载框。
前端实现跨域下载的方式有两种,使用iframe或者form表单。这两种方式,实际上都是让前端可以发起一个跨域请求,而能否下载需要后台设置以上的response header。
iframe下载
Iframe只支持GET请求,可以跨域是因为支持请求第三方资源。
export function iframeDownload(url, defaultMime = 'application/octet-stream') {
//do iframe dataURL download (old ch+FF):
const f = document.createElement('iframe');
document.body.appendChild(f);
if (/^data:/.test(url)) {
// force a mime that will download:
url = 'data:' + url.replace(/^data:([\w/-+]+)/, defaultMime);
}
f.src = url;
setTimeout(function () {
document.body.removeChild(f);
}, 333);
}
Form表单下载
Form表单之所以可以跨域,是因为JS无法获取到action后的内容,提交的form表单数据不需要返回,浏览器认为是安全的行为,所以浏览器不会阻止Form表单跨域。
Form表单可以支持GET请求和POST请求。
export function downloadFileByForm(
url: string,
filename: string,
method = 'get'
) {
const form = document.createElement('form');
form.setAttribute('action', `${url}&bucketName=${config.bucketName}`);
form.setAttribute('method', `${method}`);
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'filename');
input.setAttribute('value', `${filename}`);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
setTimeout(() => {
document.body.removeChild(form);
}, 100);
}
NPM库推荐
墙裂推荐大家看看这几个仓库的源码实现,代码都非常精简,而且实现也相对简单,但是我们却可以从中学习到不少知识。
| Npm package | Address | MINIFIED + GZIPPED | Advantage |
|---|---|---|---|
| downloadjs | github.com/rndme/downl… | 1.3kB | 支持URL、File、Blob、DataUrl、ArrayBuffer |
| file-saver | github.com/eligrey/Fil… | 1.3kB | 支持URL、File、Blob、DataUrl、ArrayBuffer |
| streamsaver | github.com/jimmywartin… | 1.7kB | 通过流的方式支持大文件下载 |
结束语
以上,便是前端下载的全部内容了。内容不多,内容简单,希望可以帮助到大家。
另外,如果需要在生产环境下实现完成的前端下载,个人认为,单单使用上面的某个库是不够的。完成的下载流程应该包括但不限于
- 支持同源下载。如上面的downloadjs和file-saver
- 支持跨域下载。通过Node中间件转发或者后台实现。
- 支持大文件下载。如上面的streamsaver。
如果实现以上流程,便是一个较为完备的下载流程了。
文章引用知乎@凯斯zhuanlan.zhihu.com/p/450942203