本文转载自微信公众号《全栈修仙之路》,作者阿宝哥。转载本文请联系全栈修真之路公众号。FileSaver.js是一种用于在客户端保存文件的解决方案,非常适合在客户端生成文件的Web应用程序。它易于使用并与大多数浏览器兼容,并在63,000个项目中用作项目依赖项。在最近的一个项目中,阿宝哥又用到了,所以想写一篇文章来谈谈这个优秀的开源项目。一、FileSaver.js简介FileSaver.js是HTML5中saveAs()FileSaver的实现。支持大部分主流浏览器,兼容性如下图:(图片来源:https://github.com/eligrey/FileSaver.js)1.1saveAsAPIFileSaversaveAs(Blob/File/Url,optionalDOMStringfilename,optionalObject{autoBom})saveAs方法支持三个参数,第一个参数表示支持Blob/File/Url三种类型,第二个参数表示文件名(可选),第三个参数表示配置对象(可选)。如果你希望FlieSaver.js自动提供Unicode文本编码提示(参见:字节顺序标记),你需要设置{autoBom:true}。1.2保存文本letblob=newBlob(["大家好,我是宝哥!"],{type:"text/plain;charset=utf-8"});FileSaver.saveAs(blob,"hello.txt");1.3保存在线资源FileSaversaveAs(Blob/File/Url,optionalDOMStringfilename,optionalObject{autoBom})如果下载的URL地址与当前站点同域,则使用a[download]进行下载。否则,首先使用同步HEAD请求判断是否支持CORS机制,支持则下载数据,使用BlobURL下载文件。如果不支持CORS机制,会尝试使用[download]方式下载。标准的W3CFileAPIBlob接口并不是所有浏览器都可用,针对这个问题,可以考虑使用Blob.js来解决兼容性问题。(图片来源:https://caniuse.com/?search=blob)1.4保存Canvas内容letcanvas=document.getElementById("my-canvas");canvas.toBlob(function(blob){saveAs(blob,"abao.png");});需要注意的是,canvas.toBlob()方法并非在所有浏览器中都可用。对于这个问题,可以考虑使用canvas-toBlob.js来解决兼容性问题。(图片来源:https://caniuse.com/?search=toBlob)在上面的例子中,我们多次见到Blob,所以在介绍FileSaver.js的源码时,包哥先简单介绍一下Blob的相关知识。2.Blob简介Blob(BinaryLargeObject)代表二进制类型的大对象。在数据库管理系统中,二进制数据存储为单个实体的集合。Blob通常是图像、声音或多媒体文件。Blob类型的对象表示不可变的原始数据,如JavaScript中的文件对象。2.1Blob构造器Blob由一个可选的字符串类型(通常是MIME类型)和blobParts组成:MIME(MultipurposeInternetMailExtensions)多用途Internet邮件扩展类型是一个应用程序设置一个具有一定扩展名的文件打开方式的类型程序.访问扩展文件时,浏览器会自动使用指定的应用程序打开。多用于指定一些客户端自定义的文件名和一些媒体文件的打开方式。常见的MIME类型有:超文本标记语言text.htmltext/html、PNG图片.pngimage/png、纯文本.txttext/plain等。在JavaScript中,我们可以通过Blob构造函数创建一个Blob对象。Blob构造函数的语法如下:varaBlob=newBlob(blobParts,options);相关参数如下:此类对象的数组。DOMString编码为UTF-8。options:包含以下两个属性的可选对象:type-默认值为“”,表示将放入blob的数组内容的MIME类型。endings–默认为“transparent”,指定如何编写包含行结尾\n的字符串。它是以下两个值之一:“native”,这意味着行尾更改为适合主机操作系统文件系统的换行符,或“transparent”,这意味着blob中保存的结束字符保持不变。现在我们已经介绍了blob,让我们来谈谈blobURL。2.2BlobURLBlobURL/ObjectURL是一个伪协议,允许Blob和File对象作为图片的URL来源,下载二进制数据的链接等。在浏览器中,我们使用URL.createObjectURL方法创建一个Blob网址。此方法接收一个Blob对象并以blob:/的形式为其创建一个唯一的URL。对应的例子如下:blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641浏览器内部为每个通过URL.createObjectURL生成的URL存储一个URL→Blob映射。因此,此类URL更短,但可以访问blob。生成的URL仅在当前文档打开时有效。它允许在中引用blob,但如果您访问不再存在的blobURL,您将从浏览器收到404错误。上面的blobURL看起来是个好主意,但它实际上有副作用。虽然存储了URL→Blob映射,但Blob本身仍然驻留在内存中,浏览器无法释放它。卸载文档时会自动清除映射,因此之后会释放Blob对象。但如果应用程序长期存在,那不会很快发生。因此,如果我们创建一个blobURL,即使不再需要该blob,它也会存在于内存中。为此,我们可以调用URL.revokeObjectURL(url)方法,该方法从内部映射中删除引用,允许删除blob(如果没有其他引用),并释放内存。好的,现在我们已经介绍了blob和blobURL。如果你还不满足,想深入了解Blob,可以阅读你所不知道的Blob一文,然后我们开始分析FileSaver.js的源码。三、FileSaver.js源码分析FileSaver.js提供了三种保存文件的方案,接下来我们将介绍这三种方案。3.1解决方案1??FileSaver.js在保存文件时,如果a标签在当前平台支持download属性,不在MacOSWebView环境下,会优先使用a[download]保存文件。在具体的使用过程中,我们通过调用saveAs方法来保存文件,其定义如下:方法支持字符型参数有两种类型:String和Blob,所以这两类参数需要在saveAs方法中分别处理。我们先分析一下字符串参数的情况。3.1.1String类型参数在前面的例子中,我们演示了如何使用saveAs方法保存在线图片:FileSaver.saveAs("https://httpbin.org/image","image.jpg");方案一中saveAs方法的处理逻辑如下://Usedownloadattributefirstifpossible(#193Lumiamobile)unlessthisisamacOSWebViewfunctionsaveAs(blob,name,opts){varURL=_global.URL||_global.webkitURL;vara=document.createElement("a");name=name||blob.name||"download";a.download=name;a.rel="noopener";if(typeofblob==="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机制,如果支持,会调用download方法下载文件。首先我们来分析一下corsEnabled方法:>=200&&xhr.status<=299;}corsEnabled方法的实现非常简单。就是通过XMLHttpRequestAPI发起一个同步的HEAD请求,然后判断返回的状态码是否在[200~299]范围内。然后我们看一下下载方法的具体实现:functiondownload(url,name,opts){varxhr=newXMLHttpRequest();xhr.open("GET",url);xhr.responseType="blob";xhr。onload=function(){saveAs(xhr.response,name,opts);};xhr.onerror=function(){console.error("couldnotdownloadfile");};xhr.send();}执行下载方法也很简单,同样是通过XMLHttpRequestAPI发起HTTP请求。与熟悉的JSON格式不同,我们需要将responseType的类型设置为blob。另外,由于返回的结果是blob类型的数据,所以成功回调函数内部会继续调用saveAs方法保存文件。对于不支持CORS机制或者同域的,会调用内部的点击方法完成下载功能。该方法的具体实现如下://`a.click()`doesn'tworkforallbrowsers(#465)functionclick(node){try{node.dispatchEvent(newMouseEvent("click"));}catch(e){varevt=document.createEvent("MouseEvents");evt.initMouseEvent("点击",true,true,window,0,0,0,80,20,false,false,false,false,0,null);节点.dispatchEvent(evt);}}在click方法中,会先调用node对象的dispatchEvent方法来派发click事件。当异常发生时,会在catch语句中进行相应的异常处理,catch语句中的MouseEvent.initMouseEvent()方法用于初始化鼠标事件的值。但需要注意的是,该功能已从web标准中删除。虽然有些浏览器仍然支持它,但在未来的某个时候可能会停止支持它。请尽量不要使用此功能。3.1.2blob类型参数同样,在前面的例子中,我们演示了如何使用saveAs方法保存Blob类型数据:letblob=newBlob(["大家好,我是阿宝哥!"],{type:"text/plain;charset=utf-8"});FileSaver.saveAs(blob,"hello.txt");blob类型参数的处理逻辑定义在saveAs方法体的else分支中://Usedownloadattributefirstifpossible(#193Lumiamobile)unlessthisisamacOSWebViewfunctionsaveAs(blob,name,opts){varURL=_global.URL||_global.webkitURL;vara=document.createElement("a");name=name||blob.name||"download";a.download=name;a.rel="noopener";if(typeofblob==="string"){//省略处理字符串类型参数}else{a.href=URL.createObjectURL(blob);setTimeout(function(){URL.revokeObjectURL(a.href);},4e4);//40ssetTimeout(function(){click(a);},0);}}对于blob类型的参数,会先通过createObjectURL方法创建ObjectURL,然后通过click方法进行File保存。为了及时释放内存,在else处理分支中,会启动一个定时器进行清理操作。至此,我们已经介绍了第一种方案,接下来要介绍的第二种方案主要是为了兼容IE浏览器。3.2解决方案2在InternetExplorer10浏览器中,msSaveBlob和msSaveOrOpenBlob方法允许用户在客户端保存文件。msSaveBlob方法只提供了一个保存按钮,而msSaveOrOpenBlob方法提供了保存和打开按钮。对应的用法如下:window.navigator.msSaveBlob(blobObject,'msSaveBlob_hello.txt');window.navigator.msSaveOrOpenBlob(blobObject,'msSaveBlobOrOpenBlob_hello.txt');了解了以上知识和方案一介绍的corsEnabled、下载、点击方法后,我们再来看一下方案二的代码,就很清楚了。当满足导航器条件中的“msSaveOrOpenBlob”时,FileSaver.js将使用第二个选项来保存文件。和之前一样,我们先来分析一下string类型参数的处理逻辑。3.2.1字符串类型参数//UsemsSaveOrOpenBlobasasecondapproachfunctionsaveAs(blob,name,opts){name=name||blob.name||"download";if(typeofblob==="string"){if(corsEnabled(blob)){//判断是否支持CORSdownload(blob,name,opts);}else{vara=document.createElement("a");a.href=blob;a.target="_blank";setTimeout(function(){click(a);});}}else{//省略处理Blob类型参数}}3.2.2Blob类型参数//UsemsSave或OpenBlobasasecondapproachfunctionsaveAs(blob,name,opts){name=name||blob.name||"download";if(typeofblob==="string"){//省略处理字符串类型参数}else{navigator.msSaveOrOpenBlob(bom(blob,opts),name);//提供保存和打开按钮}}3.3方案3If方案1和方案2都不支持,FileSaver.js会降级为使用FileReaderAPI和openAPI打开新窗口实现文件保存。3.3.1字符串类型参数//FallbacktousingFileReaderandpopupfunctionsaveAs(blob,name,opts,popup){//Openapopupimmediatelydogoaroundpopupblocker//MostlyonlyonlyavailableonuserinteractionandthefileReaderisasyncso...popup=popup||open("","_blank");if(popup){popup.document.title=popup.document.body.innerText="downloading...";}if(typeofblob==="string")returndownload(blob,name,opts);//处理Blob类型参数}3.3.2blobtypeparameters对于blob类型的参数,saveAs方法中会根据不同的环境选择不同的方案。例如,在Safari浏览器环境中,它会先使用FileReaderAPI将Blob对象转换为DataURL,然后再将DataURL地址赋值给新打开的窗口或当前窗口的location对象。具体代码如下://FallbacktousingFileReaderandpopupfunctionsaveAs(blob,name,opts,popup){//Openapopupimmediatelydogoaroundpopupblocker//MostlyonlyavailableonuserinteractionandthefileReaderisasyncso...popup=popup||open("","_blank");if(popup){//设置新打开窗口的标题popup.document.title=popup.document.body.innerText="downloading...";}if(typeofblob==="string")returndownload(blob,name,opts);varforce=blob.type==="application/octet-stream";//二进制流数据varisSafari=/constructor/i.test(_global.HTMLElement)||_global.safari;varisChromeIOS=/CriOS\/[\d]+/.test(navigator.userAgent);if((isChromeIOS||(force&&isSafari)||isMacOSWebView)&&typeofFileReader!=="undefined"){//Safarido不允许下载blobURLsvarreader=newFileReader();reader.onloadend=function(){varurl=reader.result;url=isChromeIOS?url:url.replace(/^data:[^;]*;/,"data:attachment/file;");//作为附件处理if(popup)popup.location.href=url;elselocation=url;popup=null;//reverse-tabnabbing#460};reader.readAsDataURL(blob);}else{//省略处理ObjectURLLogic}}其实对于FileReaderAPI,除了支持将File/Blob对象转换为DataURL外,还提供了readAsArrayBuffer()和readAsText()方法将File/Blob对象转换为其他数据格式在《玩转前端二进制》一文中,阿宝详细介绍了FileReaderAPI在前端图片处理场景中的应用。看完本文,你将能够轻松理解下面的转换图:最后,我们看一下else分支代码:functionsaveAs(blob,name,opts,popup){popup=popup||open("","_blank");if(popup){popup.document.title=popup.document.body.innerText="downloading...";}//处理字符串类型参数if(typeofblob==="string")returndownload(blob,name,opts);if((isChromeIOS||(force&&isSafari)||isMacOSWebView)&&typeofFileReader!=="undefined"){//省略FileReaderAPI处理逻辑}else{varURL=_global.URL||_global.webkitURL;varurl=URL.createObjectURL(blob);if(popup)popup.location=url;elselocation.href=url;popup=null;//reverse-tabnabbing#460setTimeout(function(){URL.revokeObjectURL(url);},4e4);//40s}}这里是FileSaver.js这个库的源码已经和阿宝哥一起分析阅读过了上面的源码,是不是觉得写一个兼容性好又容易的第三方库不容易使用。在实际项目中,如果需要保存超过blob大小限制的超大文件,或者内存空间不足,可以考虑使用更高级的StreamSaver.js库来实现文件保存功能。
