0%

JS 下载文件探索

我们知道,页面上展示的文件(图片)常见的有:文件的链接、base64格式的图片。文件链接是一个地址,base64格式的是一个字符串描述的文件,我们可以直接打开,或者浏览器里面直接右击下载。假设我们有需求是用户不想或者不知道浏览器可以直接下载图片,他希望出来一个按钮,点击按钮即可下载,那么我们该如何处理?

下载有链接的图片

假设有文件的地址,我们可以直接使用创建连接,然后模拟点击的操作来下载:

1
2
3
4
5
6
7
8
9
10
11
12
13

// url是我们可以获得到的下载地址
function downloadFile(url, name) {
let eleLink = document.createElement('a');
eleLink.href = url;
eleLink.download = name && undefined;
// 受浏览器安全策略的因素,动态创建的元素必须添加到浏览器后才能实施点击
document.body.appendChild(eleLink);
// 触发点击
eleLink.click();
// 然后移除
document.body.removeChild(eleLink);
}

那么问题来了,为什么a标签的点击可以直接下载呢?

HTML <a>元素可以通过它的href属性创建通向其他网页、文件、同一页面内的位置、电子邮件地址或其他任何URL的超链接。如果存在href属性,当<a>元素聚焦时按下回车键就可以激活它(或者直接点击)。

<a>标签除了常用的href属性外,还有download属性,这个属性指示浏览器下载url而不是导航到它,因此将提示用户将其保存为本地文件。如果属性有一个值,那么此值将在下载过程中作为预填充的文件名(如果用户需要,仍然可以更改文件名)。

需要注意的是,download属性只适用于同源URL。如果不同源,仍然会导航到该地址。

下载base64格式的图片

我们有些图片不是直接从服务器请求地址,而是一串base64格式的字符,那么我们如何下载这种格式的图片?

首先,我们的base64的图片是一个描述图片的字符序列,而我们要下载的是图片,是文件,那么数据文本如何转换为文件?我们需要借助Blob

Blob表示一个不可变的、原始数据的类文件对象,File接口基于Blob。我们可以使用window.URL.createObjectURL(blob)来创建图片的链接,然后接下来的操作就是创建<a>标签,模拟点击下载即可。

那关键的步骤就是如何将base64的数据转换为Blob

我们base64格式的数据是一个Data URLs数据,即前缀为data:协议的URL,其允许内容创建者向文档中嵌入小文件。

Data URLs由四个部分组成:

  • 前缀(data:)
  • 指示数据类型的MIME类型
  • 如果非文本则可选的base64标记
  • 数据本身
1
data:[<mediatype>][;base64],<data>

mediatype是个MIME类型的字符串,如image/jpeg表示JPEG图像的文件。如果被省略,则默认为text/plain;charset=US-ASCII

我们来查看一个base64描述的二维码:

1
data:image/png;base64,iVBORw0KGgoAAAANS.....yBpoptXl+gmwNKw/sDrSJjDtDFcosAAAAASUVORK5CYII=

我们再看下base64数据是啥。

base64是一组类似于二进制文本的编码规则,使得二进制数据在解释成radix-64的表现形式后能够用ASCII字符串格式表示出来。base64编码普遍用于需要通过被设计为处理文本数据的媒介上存储和传输二进制数据而需要编码该二进制数据的场景。这样是为了保证数据的完整并且不用在传输过程中修改这些数据。base64也被一些应用(包括使用MIME的垫资邮件)和在XML中存储复杂数据时使用。
我们可以通过atob()函数解码通过base64编码的字符串数据,通过btoa()函数能够从二进制数据创建一个base64编码的字符串.

ps:a 代表 ASCII码,b 代表二进制。

我们对上面的base64格式的图像进行解码:
atob base64

可以看到解码后的数据为二进制数据。每一个base64字符实际上代表着6比特位,因此,3字节(一字节是8比特,3字节就是24比特)的字符串(二进制文件)可以转换为4个base64字符(4x6=24bit)。这意味着转换为base64格式的字符串相比原始尺寸增加了大约33%。如果编码的数据很少,可能增加的体积就越大。

有意思的是,如果bit数不能被4整除,需要在末尾加1或2个byte,并且末尾的0不能使用A而使用=。这就是为什么base64有的编码后会有一两个等号的缘由。

那么解码后的二进制字符串如何和blob结合?

我们可以看下Blob的构造函数:

1
var aBlob = new Blob(array, options)

参数:

  • array 是一个由ArrayBufferArrayBufferViewBlobDOMString等对象构成的Array,或者其他类对象混合体。DOMString会被编码为UTF-8.
  • options 是一个可选的BlobPropertyBag字典,主要需要type,它代表被放入Blob中的数组内容的MIME类型。

我们知道传入的应该是一个二进制数据缓冲区。我们无法直接操作ArrayBuffer,而是通过类型数组对象或者DataView对象来操作,它们会将缓冲区的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

JavaScript类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。

类型化数组是一种类似数组的对象,并提供一种用于访问原始二进制数据的机制。和Array相比,类型数组针对于处理音频、视频、文件的操作,很明显会更好。

为达到最大的灵活性和效率,JavaScript类型数组将实现拆分为缓冲视图两部分。一个缓冲(由ArrayBuffer实现)描述的是一个数据块,缓冲没有格式可言,并且不提供机制访问其内容。为了访问和操作缓冲,我们需要使用视图。视图提供了上下文–即数据类型、起始偏移量和元素数——将数据转换为实际有类型的数组。类型数组架构图:
array buffer

我们可以借助Unit8Array类型数组。Unit8Array数组类型表示一个8位无符号型数组,创建时内容被初始化为0,创建完后,可以以对象的方式或以数组方式引用数组中的元素。

综合上面的思路,我们可以实现一个转换方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 从dataURI中获得图片的类型
function getType (dataURI) {
const result = /data:image\/(\S+);/.exec(dataURI);
if (result) {
return result[1]
}
return undefined;
}

function dataURItoBlob(dataURI) {
const binary = atob(dataURI.split(',')[1]);
const type = getType(dataURI)
if (!type) {
return
}
const array = [];
for (var i = 0; i < binary.length; i++){
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {type: type});
}

这样我们就可以将base64格式描述的图片转换为Blob对象。但是我们创建的对象只是存在于内存中的类文件对象,没法在放在页面上。我们需要通过URL.createObjectURL()方法来将文件和DOM进行关联。

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

需要注意的是,使用URL.createObjectURL()方法都会创建一个新的URL对象,即使已经用相同的对象参数创建过。所以当不需要这个URL对象时,需要调用URL.revokeObjectURL()方法来释放。注意,释放的时候传递给revokeObjectURL方法的参数应当是创建出来的URL对象DOMString

那么我们的下载方法可以有:

1
2
3
4
5
6
7
8
9
10
11
function downLoadFile(fileName, uri) {
var eleLink = document.createElement('a');
const type = getType(uri)
// 因为这里需要下载的文件的后缀,所以将获取图片类型的参数提取出来,传递给dataURIToBlob方法。
var blob = dataURItoBlob(uri, type);
eleLink.download = `${fileName}.${type}`;
eleLink.href = URL.createObjectURL(blob);
eleLink.click();
URL.revokeObjectURL(eleLink.href);
document.body.removeChild(eleLink)
}

最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function getType (dataURI) {
const result = /data:image\/(\S+);/.exec(dataURI);
if (result) {
return result[1]
}
return undefined;
}

function dataURItoBlob(dataURI, type) {
const binary = atob(dataURI.split(',')[1]);
const array = [];
for (var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
console.log(type)
return new Blob([new Uint8Array(array)], {type: type});
}

function downLoadFile(fileName, uri) {
const eleLink = document.createElement('a');
const type = getType(uri)
const blob = dataURItoBlob(uri, type);
eleLink.download = `${fileName}.${type}`;
eleLink.href = URL.createObjectURL(blob);
eleLink.click();
URL.revokeObjectURL(eleLink.href);
document.body.removeChild(eleLink)
}

// 调用的时候,只需要传递给downloadFile方法一个文件名,一个图片的uri
downloadFile('二维码', uri)

经过漫长的搜索和查对应的方法,终于搞清楚了blob和ArrayBuffer之间的关系,以及如何转换。受益匪浅啊!

参考:
MDN Base64的编码与解码
MDN JavaScript类型化数组
MDN Blob

码字辛苦,打赏个咖啡☕️可好?💘