在上面的例子中,我们多次见到了Blob,所以在介绍.js源码时,阿宝哥先简单介绍一下Blob的相关知识。
2.Blob介绍
Blob ( Large ) 表示二进制类型的大对象。 在数据库管理系统中,二进制数据存储为单个实体的集合。 Blob 通常是图像、声音或多媒体文件。 Blob 类型的对象表示不可变的原始数据,如文件对象。
2.1 Blob构造函数
Blob 由可选的字符串类型(通常是 MIME 类型)和:
MIME(邮件)多用途 邮件扩展类型是一种设置要使用应用程序打开的具有特定扩展名的文件的方法。 当访问具有此扩展名的文件时,浏览器将自动使用指定的应用程序打开它。 打开。 主要用于指定一些客户端定义的文件名和一些媒体文件的打开方式。
常见的MIME类型有:超文本标记语言text.html text/html、PNG image.png image/png、普通text.txt text/plain等。
在 中,我们可以通过 Blob 构造函数创建一个 Blob 对象。 Blob构造函数的语法如下:
var aBlob = new Blob(blobParts, options);
相关参数说明如下:
现在我们已经介绍了 Blob,接下来我们来谈谈 Blob URL。
2.2 Blob URL
Blob URL/ URL 是一种伪协议,允许 Blob 和 File 对象用作图像的 URL 源、下载二进制数据的链接等。在浏览器中,我们使用 URL。 方法来创建 Blob URL。 此方法接收 Blob 对象并为其创建唯一的 URL,格式为 blob:/。 对应的例子如下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
浏览器内部为通过 URL 生成的每个 URL 存储一个 URL → Blob 映射。 因此,此类 URL 较短,但可以访问 blob。 生成的 URL 仅在当前文档打开时有效。它允许引用
,但是如果您访问不再存在的 blob URL,您的浏览器将会收到 404 错误。
上面的 blob URL 可能看起来是个好主意,但它实际上有副作用。 当 URL → Blob 映射被存储时,Blob 本身仍然驻留在内存中,浏览器无法释放它。 卸载文档时,映射会自动清除,因此 blob 对象也会被释放。 但如果应用程序的寿命很长,那么这种情况不会很快发生。 因此,如果我们创建一个 blob URL,即使不再需要该 blob,它也会存在于内存中。
为此,我们可以调用 URL.(url) 方法,该方法从内部映射中删除引用,允许删除 blob(如果没有其他引用),并释放内存。
好的,现在我们已经介绍了 blob 和 blob URL。 如果你还没看完,想深入了解Blob,可以阅读这篇文章,然后我们开始分析.js的源码。
如果你想了解阅读源码的思路和技巧,可以阅读这篇文章。
3..js源码分析
.js内部提供了三种方案来实现文件保存,所以接下来我们将分别介绍这三种方案。
3.1 方案1
.js保存文件时,如果a标签支持当前平台的属性,且环境不是MacOS,会先使用a[]保存文件。 在具体使用过程中,我们通过调用方法来保存文件,其定义如下:
FileSaver saveAs(Blob/File/Url, optional DOMString filename, optional Object { autoBom })
通过观察方法的签名,我们知道该方法支持字符串和blob两种类型的参数,因此在方法内部需要对这两类参数进行单独处理。 我们先来分析一下字符串参数的情况。
3.1.1 字符串类型参数
在前面的例子中,我们演示了如何使用该方法来保存图像就行了:
FileSaver.saveAs("https://httpbin.org/image", "image.jpg");
方案一中,该方法的处理逻辑如下:
// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL;
var a = document.createElement("a");
name = name || blob.name || "download";
a.download = name;
a.rel = "noopener";
if (typeof blob === "string") {
a.href = blob;
if (a.origin !== location.origin) { // (1)
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, (a.target = "_blank"));
} else { // (2)
click(a);
}
} else {
// 省略处理Blob类型参数
}
}
上面的代码中,如果发现下载资源的URL地址与当前站点不在同一个域,则会通过同步HEAD请求来判断是否支持CORS机制,如果支持,则该方法将被调用来下载文件。 首先我们先来分析一下方法:
function corsEnabled(url) {
var xhr = new XMLHttpRequest();
xhr.open("HEAD", url, false);
try {
xhr.send();
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299;
}
该方法的实现很简单,就是通过API发起同步HEAD请求,然后判断返回的状态码是否在[200 ~ 299]范围内。 接下来我们看一下该方法的具体实现:
function download(url, name, opts) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = function () {
saveAs(xhr.response, name, opts);
};
xhr.onerror = function () {
console.error("could not download file");
};
xhr.send();
}
同样方法的实现也很简单,也是通过API发起HTTP请求。 与我们熟悉的JSON格式不同的是,我们需要设置的类型是blob。 另外,由于返回的结果是blob类型数据,因此成功回调函数内部会继续调用该方法来保存文件。
但如果不支持CORS机制或者同域,则会调用内部的click方法来完成下载功能。 该方法的具体实现如下:
// `a.click()` doesn't work for all browsers (#465)
function click(node) {
try {
node.dispatchEvent(new MouseEvent("click"));
} catch (e) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(
"click", true, true, window, 0, 0, 0, 80, 20,
false, false, false, false, 0, null
);
node.dispatchEvent(evt);
}
}
在 click 方法中,将首先调用节点对象上的方法来调度 click 事件。 当异常发生时,catch语句中会进行相应的异常处理,catch语句中的.()方法用于初始化鼠标事件的值。 但需要注意的是,该功能已从 Web 标准中删除。 尽管某些浏览器仍然支持它,但将来可能会停止支持。 请尽量不要使用此功能。
3.1.2 blob类型参数
同样,在前面的例子中,我们演示了如何使用方法来保存Blob类型数据:
let blob = new Blob(["大家好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, "hello.txt");
blob类型参数的处理逻辑定义在方法体的else分支中:
// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
function saveAs(blob, name, opts) {
var URL = _global.URL || _global.webkitURL;
var a = document.createElement("a");
name = name || blob.name || "download";
a.download = name;
a.rel = "noopener";
if (typeof blob === "string") {
// 省略处理字符串类型参数
} else {
a.href = URL.createObjectURL(blob);
setTimeout(function () {
URL.revokeObjectURL(a.href);
}, 4e4); // 40s
setTimeout(function () {
click(a);
}, 0);
}
}
对于blob类型的参数,首先通过方法创建URL,然后通过click方法保存文件。 为了及时释放内存,在else处理分支中,会启动一个定时器来执行清理操作。 至此,我们已经介绍了第一个解决方案,接下来要介绍的第二个解决方案主要是为了兼容IE浏览器。
3.2 方案2
在 10 种浏览器中, 和 方法允许用户在客户端保存文件。 该方法只提供了保存按钮,而该方法提供了保存和打开按钮。 对应的用法如下:
window.navigator.msSaveBlob(blobObject, 'msSaveBlob_hello.txt');
window.navigator.msSaveOrOpenBlob(blobObject, 'msSaveBlobOrOpenBlob_hello.txt');
了解了上面的知识以及解决方案一中介绍的 、 、 click 方法后,再看解决方案二的代码,就会很清楚了。
当满足条件中的“”时,.js将使用第二种方案来保存文件。 和之前一样,我们先来分析一下字符串类型参数的处理逻辑。
3.2.1 字符串类型参数
// Use msSaveOrOpenBlob as a second approach
function saveAs(blob, name, opts) {
name = name || blob.name || "download";
if (typeof blob === "string") {
if (corsEnabled(blob)) { // 判断是否支持CORS
download(blob, name, opts);
} else {
var a = document.createElement("a");
a.href = blob;
a.target = "_blank";
setTimeout(function () {
click(a);
});
}
} else {
// 省略处理Blob类型参数
}
}
3.2.2 blob类型参数
// Use msSaveOrOpenBlob as a second approach
function saveAs(blob, name, opts) {
name = name || blob.name || "download";
if (typeof blob === "string") {
// 省略处理字符串类型参数
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name); // 提供了保存和打开按钮
}
}
3.3 方案3
如果选项 1 和选项 2 都不支持,.js 将降级为使用 API 并打开 API 打开新窗口来保存文件。
3.3.1 字符串类型参数
// Fallback to using FileReader and a popup
function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open("", "_blank");
if (popup) {
popup.document.title = popup.document.body.innerText = "downloading...";
}
if (typeof blob === "string") return download(blob, name, opts);
// 处理Blob类型参数
}
3.3.2 blob类型参数
对于blob类型的参数,会根据方法内部的不同环境选择不同的方案。 比如在浏览器环境下,它会先使用API将Blob对象转换为Data URL,然后将Data URL地址赋给新打开的窗口或者当前窗口的对象,具体代码如下:
// Fallback to using FileReader and a popup
function saveAs(blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open("", "_blank");
if (popup) { // 设置新开窗口的标题
popup.document.title = popup.document.body.innerText = "downloading...";
}
if (typeof blob === "string") return download(blob, name, opts);
var force = blob.type === "application/octet-stream"; // 二进制流数据
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
if (
(isChromeIOS || (force && isSafari) || isMacOSWebView) &&
typeof FileReader !== "undefined"
) {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader();
reader.onloadend = function () {
var url = reader.result;
url = isChromeIOS
? url
: url.replace(/^data:[^;]*;/, "data:attachment/file;"); // 处理成附件的形式
if (popup) popup.location.href = url;
else location = url;
popup = null; // reverse-tabnabbing #460
};
reader.readAsDataURL(blob);
} else {
// 省略Object URL的处理逻辑
}
}
事实上,对于 API 来说,除了支持 File/Blob 对象转换为 Data URL 之外,还提供了 () 和 () 方法将 File/Blob 对象转换为其他数据格式。
阿宝哥在文章中详细介绍了该API在前端图像处理场景中的应用。 读完文章后,您将能够轻松理解下面的转换图:
最后我们看一下else分支的代码:
function saveAs(blob, name, opts, popup) {
popup = popup || open("", "_blank");
if (popup) {
popup.document.title = popup.document.body.innerText = "downloading...";
}
// 处理字符串类型参数
if (typeof blob === "string") return download(blob, name, opts);
if (
(isChromeIOS || (force && isSafari) || isMacOSWebView) &&
typeof FileReader !== "undefined"
) {
// 省略FileReader API处理逻辑
} else {
var URL = _global.URL || _global.webkitURL;
var url = URL.createObjectURL(blob);
if (popup) popup.location = url;
else location.href = url;
popup = null; // reverse-tabnabbing #460
setTimeout(function () {
URL.revokeObjectURL(url);
}, 4e4); // 40s
}
}
至此,.js库的源码就已经分析完毕。 和阿宝哥一起看完了上面的源码,是不是觉得写一个兼容性好、好用的第三方库不容易呢?
在实际项目中,如果需要保存非常大的文件,超过了blob大小限制,或者内存空间不够,可以考虑使用更高级的.js库来实现文件保存功能。
4. 参考资源
聚焦全栈,重点分享、Web API、前端架构等技术干货。