文件下载,搞懂这9种场景就够了(上)
既然掘友有要求,连标题也帮阿宝哥想好了,那我们就来整一篇文章,总结一下文件下载的场景。
一般在我们工作中,主要会涉及到 9 种文件下载的场景,每一种场景背后都使用不同的技术,其中也有很多细节需要我们额外注意。今天阿宝哥就来带大家总结一下这 9 种场景,让大家能够轻松地应对各种下载场景。阅读本文后,你将会了解以下的内容:
在浏览器端处理文件的时候,我们经常会用到 Blob 。比如图片本地预览、图片压缩、大文件分块上传及文件下载。在浏览器端文件下载的场景中,比如我们今天要讲到的 a 标签下载、showSaveFilePicker API 下载、Zip 下载 等场景中,都会使用到 Blob ,所以我们有必要在学习具体应用前,先掌握它的相关知识,这样可以帮助我们更好地了解示例代码。
一、基础知识
1.1 了解 Blob
Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示一个不可变、原始数据的类文件对象。 它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 用于数据操作。
Blob
对象由一个可选的字符串 type
(通常是 MIME 类型)和 blobParts
组成:
在 JavaScript 中你可以通过 Blob 的构造函数来创建 Blob 对象,Blob 构造函数的语法如下:
const aBlob = new Blob(blobParts, options);
相关的参数说明如下:
- blobParts:它是一个由 ArrayBuffer,ArrayBufferView,Blob,DOMString 等对象构成的数组。DOMStrings 会被编码为 UTF-8。
- options:一个可选的对象,包含以下两个属性:
- type —— 默认值为
""
,它代表了将会被放入到 blob 中的数组内容的 MIME 类型。 - endings —— 默认值为
"transparent"
,用于指定包含行结束符\n
的字符串如何被写入。 它是以下两个值中的一个:"native"
,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者"transparent"
,代表会保持 blob 中保存的结束符不变。
1.2 了解 Blob URL
Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像、下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL
方法来创建 Blob URL,该方法接收一个 Blob
对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>
,对应的示例如下:
blob:http://localhost:3000/53acc2b6-f47b-450f-a390-bf0665e04e59
浏览器内部为每个通过 URL.createObjectURL
生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 较短,但可以访问 Blob
。生成的 URL 仅在当前文档打开的状态下才有效。它允许引用 <img>
、<a>
中的 Blob
,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误。
上述的 Blob URL 看似很不错,但实际上它也有副作用。 虽然存储了 URL → Blob 的映射,但 Blob 本身仍驻留在内存中,浏览器无法释放它。映射在文档卸载时自动清除,因此 Blob 对象随后被释放。但是,如果应用程序寿命很长,那么 Blob 在短时间内将无法被浏览器释放。因此,如果你创建一个 Blob URL,即使不再需要该 Blob,它也会存在内存中。
针对这个问题,你可以调用 URL.revokeObjectURL(url)
方法,从内部映射中删除引用,从而允许删除 Blob(如果没有其他引用),并释放内存。
现在你已经了解了 Blob 和 Blob URL,如果你还意犹未尽,想深入理解 Blob 的话,可以阅读 你不知道的 Blob 这篇文章。下面我们开始介绍客户端文件下载的场景。
随着 Web 技术的不断发展,浏览器的功能也越来越强大。这些年出现了很多在线 Web 设计工具,比如在线 PS、在线海报设计器或在线自定义表单设计器等。这些 Web 设计器允许用户在完成设计之后,把生成的文件保存到本地,其中有一部分设计器就是利用浏览器提供的 Web API 来实现客户端文件下载。下面阿宝哥先来介绍客户端下载中,最常见的 a 标签下载 方案。
二、a 标签下载
html
<h3>a 标签下载示例</h3>
<div>
<img src="../images/body.png" />
<img src="../images/eyes.png" />
<img src="../images/mouth.png" />
</div>
<img id="mergedPic" src="http://via.placeholder.com/256" />
<button onclick="merge()">图片合成</button>
<button onclick="download()">图片下载</button>
在以上代码中,我们通过 img
标签引用了以下 3 张素材:
当用户点击 图片合成 按钮时,会将合成的图片显示在 img#mergedPic
容器中。在图片成功合成之后,用户可以通过点击 图片下载 按钮把已合成的图片下载到本地。对应的操作流程如下图所示:
由上图可知,整体的操作流程相对简单。接下来,我们来看一下 图片合成 和 图片下载 的实现逻辑。
js
图片合成的功能,阿宝哥是直接使用 Github 上 merge-images 这个第三方库来实现。利用该库提供的 mergeImages(images, [options])
方法,我们可以轻松地实现图片合成的功能。调用该方法后,会返回一个 Promise 对象,当异步操作完成后,合成的图片会以 Data URLs 的格式返回。
const mergePicEle = document.querySelector("#mergedPic");
const images = ["/body.png", "/eyes.png", "/mouth.png"].map(
(path) => "../images" + path
);
let imgDataUrl = null;
async function merge() {
imgDataUrl = await mergeImages(images);
mergePicEle.src = imgDataUrl;
}
而图片下载的功能是借助 dataUrlToBlob
和 saveFile
这两个函数来实现。它们分别用于实现 Data URLs => Blob 的转换和文件的保存,具体的代码如下所示:
function dataUrlToBlob(base64, mimeType) {
let bytes = window.atob(base64.split(",")[1]);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
// 保存文件
function saveFile(blob, filename) {
const a = document.createElement("a");
a.download = filename;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href)
}
因为本文的主题是介绍文件下载,所以我们来重点分析 saveFile
函数。在该函数内部,我们使用了 HTMLAnchorElement.download 属性,该属性值表示下载文件的名称。如果该名称不是操作系统的有效文件名,浏览器将会对其进行调整。此外,该属性的作用是表明链接的资源将被下载,而不是显示在浏览器中。
需要注意的是,download
属性存在兼容性问题,比如 IE 11 及以下的版本不支持该属性,具体如下图所示:
(图片来源:caniuse.com/download)
当设置好 a 元素的 download
属性之后,我们会调用 URL.createObjectURL
方法来创建 Object URL,并把返回的 URL 赋值给 a 元素的 href
属性。接着通过调用 a 元素的 click
方法来触发文件的下载操作,最后还会调用一次 URL.revokeObjectURL
方法,从内部映射中删除引用,从而允许删除 Blob(如果没有其他引用),并释放内存。
关于 a 标签下载 的内容就介绍到这,下面我们来介绍如何使用新的 Web API —— showSaveFilePicker
实现文件下载。
a 标签下载示例:a-tag
三、showSaveFilePicker API 下载
showSaveFilePicker API 是 Window
接口中定义的方法,调用该方法后会显示允许用户选择保存路径的文件选择器。该方法的签名如下所示:
let FileSystemFileHandle = Window.showSaveFilePicker(options);
showSaveFilePicker 方法支持一个对象类型的可选参数,可包含以下属性:
excludeAcceptAllOption
:布尔类型,默认值为false
。默认情况下,选择器应包含一个不应用任何文件类型过滤器的选项(由下面的types
选项启用)。将此选项设置为true
意味着types
选项不可用。types
:数组类型,表示允许保存的文件类型列表。数组中的每一项是包含以下属性的配置对象:description(可选)
:用于描述允许保存文件类型类别。accept
:是一个对象,该对象的key
是 MIME 类型,值是文件扩展名列表。
调用 showSaveFilePicker 方法之后,会返回一个 FileSystemFileHandle 对象。有了该对象,你就可以调用该对象上的方法来操作文件。比如调用该对象上的 createWritable 方法之后,就会返回 FileSystemWritableFileStream 对象,就可以把数据写入到文件中。具体的使用方式如下所示:
async function saveFile(blob, filename) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [
{
description: "PNG file",
accept: {
"image/png": [".png"],
},
},
{
description: "Jpeg file",
accept: {
"image/jpeg": [".jpeg"],
},
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
}
function download() {
if (!imgDataUrl) {
alert("请先合成图片");
return;
}
const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
saveFile(imgBlob, "face.png");
}
当你使用以上更新后的 saveFile
函数,来保存已合成的图片时,会显示以下保存文件选择器:
由上图可知,相比 a 标签下载 的方式,showSaveFilePicker API 允许你选择文件的下载目录、选择文件的保存格式和更改存储的文件名称。看到这里是不是觉得 showSaveFilePicker API 功能挺强大的,不过可惜的是该 API 目前的兼容性还不是很好,具体如下图所示:
(图片来源:caniuse.com/?search=sho…
其实 showSaveFilePicker 是 File System Access API 中定义的方法,除了 showSaveFilePicker 之外,还有 showOpenFilePicker 和 showDirectoryPicker 等方法。如果你想在实际项目中使用这些 API 的话,可以考虑使用 GoogleChromeLabs 开源的 browser-fs-access 这个库,该库可以让你在支持平台上更方便地使用 File System Access API,对于不支持的平台会自动降级使用 <input type="file">
和 <a download>
的方式。
可能大家对 browser-fs-access 这个库会比较陌生,但是如果换成是 FileSaver.js 这个库的话,应该就比较熟悉了。接下来,我们来介绍如何利用 FileSaver.js 这个库实现客户端文件下载。
showSaveFilePicker API 下载示例:save-file-picker
四、FileSaver 下载
FileSaver.js 是在客户端保存文件的解决方案,非常适合在客户端上生成文件的 Web 应用程序。它是 HTML5 版本的 saveAs() FileSaver 实现,支持大多数主流的浏览器,其兼容性如下图所示:
(图片来源:github.com/eligrey/Fil…
在引入 FileSaver.js 这个库之后,我们就可以使用它提供的 saveAs
方法来保存文件。该方法对应的签名如下所示:
FileSaver saveAs(
Blob/File/Url,
optional DOMString filename,
optional Object { autoBom }
)
saveAs 方法支持 3 个参数,第 1 个参数表示它支持 Blob/File/Url
三种类型,第 2 个参数表示文件名(可选),而第 3 个参数表示配置对象(可选)。如果你需要 FlieSaver.js 自动提供 Unicode 文本编码提示(参考:字节顺序标记),则需要设置 { autoBom: true}
。
了解完 saveAs 方法之后,我们来举 3 个具体的使用示例:
1. 保存文本
let blob = new Blob(["大家好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
saveAs(blob, "hello.txt");
2. 保存线上资源
saveAs("https://httpbin.org/image", "image.jpg");
如果下载的 URL 地址与当前站点是同域的,则将使用 a[download]
方式下载。否则,会先使用 同步的 HEAD 请求 来判断是否支持 CORS 机制,若支持的话,将进行数据下载并使用 Blob URL 实现文件下载。如果不支持 CORS 机制的话,将会尝试使用 a[download]
方式下载。
标准的 W3C File API Blob 接口并非在所有浏览器中都可用,对于这个问题,你可以考虑使用 Blob.js 来解决兼容性问题。
(图片来源:caniuse.com/?search=blo…
3. 保存 canvas 画布内容
let canvas = document.getElementById("my-canvas");
canvas.toBlob(function(blob) {
saveAs(blob, "abao.png");
});
需要注意的是 canvas.toBlob()
方法并非在所有浏览器中都可用,对于这个问题,你可以考虑使用 canvas-toBlob.js 来解决兼容性问题。
(图片来源:caniuse.com/?search=toB…
介绍完 saveAs 方法的使用示例之后,我们来更新前面示例中的 download
方法:
function download() {
if (!imgDataUrl) {
alert("请先合成图片");
return;
}
const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
saveAs(imgBlob, "face.png");
}
很明显,使用 saveAs 方法之后,下载已合成的图片就很简单了。如果你对 FileSaver.js 的工作原理感兴趣的话,可以阅读 聊一聊 15.5K 的 FileSaver,是如何工作的? 这篇文章。前面介绍的场景都是直接下载单个文件,其实我们也可以在客户端同时下载多个文件,然后把已下载的文件压缩成 Zip 包并下载到本地。
FileSaver 下载示例:file-saver
五、Zip 下载
在 文件上传,搞懂这8种场景就够了 这篇文章中,阿宝哥介绍了如何利用 JSZip 这个库提供的 API,把待上传目录下的所有文件压缩成 ZIP 文件,然后再把生成的 ZIP 文件上传到服务器。同样,利用 JSZip 这个库,我们可以实现在客户端同时下载多个文件,然后把已下载的文件压缩成 Zip 包,并下载到本地的功能。对应的操作流程如下图所示:
在以上 Gif 图中,阿宝哥演示了把 3 张素材图,打包成 Zip 文件并下载到本地的过程。接下来,我们来介绍如何使用 JSZip 这个库实现以上的功能。
html
<h3>Zip 下载示例</h3>
<div>
<img src="../images/body.png" />
<img src="../images/eyes.png" />
<img src="../images/mouth.png" />
</div>
<button onclick="download()">打包下载</button>
js
const images = ["body.png", "eyes.png", "mouth.png"];
const imageUrls = images.map((name) => "../images/" + name);
async function download() {
let zip = new JSZip();
Promise.all(imageUrls.map(getFileContent)).then((contents) => {
contents.forEach((content, i) => {
zip.file(images[i], content);
});
zip.generateAsync({ type: "blob" }).then(function (blob) {
saveAs(blob, "material.zip");
});
});
}
// 从指定的url上下载文件内容
function getFileContent(fileUrl) {
return new JSZip.external.Promise(function (resolve, reject) {
// 调用jszip-utils库提供的getBinaryContent方法获取文件内容
JSZipUtils.getBinaryContent(fileUrl, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
在以上代码中,当用户点击 打包下载 按钮时,就会调用 download
函数。在该函数内部,会先调用 JSZip
构造函数创建 JSZip
对象,然后使用 Promise.all 函数来确保所有的文件都下载完成后,再调用 file(name, data [,options])
方法,把已下载的文件添加到前面创建的 JSZip
对象中。最后通过 zip.generateAsync
函数来生成 Zip 文件并使用 FileSaver.js 提供的 saveAs
方法保存 Zip 文件。
Zip 下载示例:Zip