關(guān)于點擊下載文件的那些事

前言

通過點擊下載部署在服務(wù)器上的文件摄欲,是B端的一個常見需求,但是為了獲得良好的體驗轿亮,其中還是有很多值得鉆研的小知識點。筆者最近在開發(fā)個人云盤胸墙,在開發(fā)文件下載功能的過程中也接觸到了這一領(lǐng)域(完成后也會發(fā)一篇博文我注,這里先占個坑)。網(wǎng)上有各種各樣的實現(xiàn)迟隅,不過也有各種各樣的問題但骨,比如跨域下載不支持,需要另開瀏覽器tab或者圖片等瀏覽器可以識別的內(nèi)容直接打開等等影響體驗的細(xì)節(jié)智袭,這里就攤開講講奔缠。

常見方法

a標(biāo)簽內(nèi)置下載

實現(xiàn)非常簡單:

<a href='下載文件的url'>點我下載</a>

相信這也是大多數(shù)人第一個想到的方法,通常情況下體驗十分完美吼野,但是如果遇到下載圖片校哎,pdf,txt等等瀏覽器能夠直接解析出來并展示的文件,就會出現(xiàn)如下的結(jié)果:

image

瀏覽器發(fā)現(xiàn)該資源可以解析后瞳步,會直接跳轉(zhuǎn)到下載資源的url并在窗口展示闷哆。這個體驗想必是大多數(shù)人不能接受的,為此我們可以添加download參數(shù)谚攒,告訴瀏覽器我們想要的是下載這個資源阳准,類似這樣:

<a href='下載文件的url' download>點我下載</a>

通過download還可以修改文件下載后的名字和類型:

<a href='下載文件的url' download='文件名.后綴名'>點我下載</a>

如果不要求修改文件的類型氛堕,后綴名可以省略:

<a href='下載文件的url' download='文件名'>點我下載</a>

download屬性就是銀彈嗎?很遺憾讓你失望了馏臭,親測在chrome下,如果資源不是同源的,download屬性是無效的括儒,加與不加一個樣绕沈,如果要解決這個問題,只有通過后端進行配合了帮寻。關(guān)于該屬性的兼容性內(nèi)容乍狐,有張鑫旭大神的總結(jié)貼了解HTML/HTML5中的download屬性可以參考,這里不贅述固逗。

window.open開啟新tab下載

該方法通過winodw.open打開新的tab浅蚪,利用瀏覽器無法解析的資源會變成下載的特性來實現(xiàn)功能,api也不復(fù)雜:

window.open('目標(biāo)url')

我們看看在普通文件下的表現(xiàn):

image

大家可以明顯看到烫罩,這里有開一個tab的過程惜傲。瀏覽器發(fā)現(xiàn)該資源無法解析,瀏覽器會關(guān)閉該tab贝攒,走下載流程
大家肯定比較關(guān)心針對圖片的表現(xiàn)盗誊,很遺憾跟a標(biāo)簽的下載一樣不盡如人意:
image

針對瀏覽器可以直接解析出來的內(nèi)容,瀏覽器會開啟tab并顯示內(nèi)容隘弊。

通過提交表單

原理上是通過構(gòu)造表單哈踱,通過submit方法向服務(wù)器請求資源:

export function downloadUrlFile(url) {
    let tempForm = document.createElement('form')
    tempForm.action = url
    tempForm.method = 'get'
    tempForm.style.display = 'none'
    document.body.appendChild(tempForm)
    tempForm.submit()
    return tempForm
}

form表單設(shè)置get方法,然后根據(jù)傳入的url向服務(wù)器請求資源梨熙,按照先前的流程开镣,我們看看正常文件的體驗:

image

體驗良好,也沒有tab的閃動串结。接下來我們看看針對圖片的體驗:
image

很遺憾哑子,針對圖片等文件還是會打開tab直接展示內(nèi)容。

全村的希望:download.js

這是國外一個大佬寫的專門針對文件下載的腳本肌割,功能非常豐富卧蜓,不僅可以下載服務(wù)器上的內(nèi)容,還可以針對base64等dataUrl形式的文件進行下載把敞,使用方法非常豐富弥奸,此處是傳送門download.js官方文檔,這里貼上源碼和筆者的簡單注釋(注意這里是html內(nèi)嵌腳本的形式展現(xiàn)的奋早,如有需要可以單獨摳出來搞成一個模塊供外部引用):

<script>//download.js v4.2, by dandavis; 2008-2016. [CCBY2] see http://danml.com/download.html for tests/usage
// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
// v4 adds AMD/UMD, commonJS, and plain browser support
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
// https://github.com/rndme/download

(function (root, factory) {
    //  兼容各種模塊寫法盛霎,在全局對象上掛載download方法
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        //  針對AMD規(guī)范,注冊一個匿名模塊
        define([], factory);
    } else if (typeof exports === 'object') {
        //  針對Node,環(huán)境耽装,不支持嚴(yán)格模式
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        //  瀏覽器全局變量支持
        // Browser globals (root is window)
        root.download = factory();
  }
}(this, function () {
    //  第一個參數(shù)是數(shù)據(jù)愤炸,第二個參數(shù)是文件名,第三個參數(shù)是mime類型
    //  下載服務(wù)器上的文件直接第一個參數(shù)傳入url即可掉奄,后兩個不用傳
    return function download(data, strFileName, strMimeType) {
        //  這里的腳本僅支持客戶端
        var self = window, // this script is only for browsers anyway...
            // 默認(rèn)的mime類型
            defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
            mimeType = strMimeType || defaultMime,
            payload = data,
            //  如果只傳入第一個參數(shù)规个,則把其解析為下載url
            url = !strFileName && !strMimeType && payload,
            //  創(chuàng)建a標(biāo)簽,方便下載
            anchor = document.createElement("a"),
            toString = function(a){return String(a);},
            //  根據(jù)瀏覽器兼容性,提取Blob
            myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
            fileName = strFileName || "download",
            blob,
            reader;
            myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
      
        //  調(diào)換參數(shù)的順序,允許download.bind(true, "text/xml", "export.xml")這種寫法
        if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
            payload=[payload, mimeType];
            mimeType=payload[0];
            payload=payload[1];
        }

        //  根據(jù)傳入的url這一個參數(shù)下載文件(必須是同源的诞仓,因為走的是XMLHttpRequest)
        if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
            //  解析出文件名
            fileName = url.split("/").pop().split("?")[0];
            //  設(shè)置a標(biāo)簽的href
            anchor.href = url; // assign href prop to temp anchor
            //  避免鏈接不可用
            if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
                // 構(gòu)造一個XMLHttpRequest請求
                var ajax=new XMLHttpRequest();
                //  設(shè)置get方法
                ajax.open( "GET", url, true);
                //  設(shè)置響應(yīng)類型為blob,避免瀏覽器直接解析出來并展示
                ajax.responseType = 'blob';
                //  設(shè)置回調(diào)
                ajax.onload= function(e){
                // 再次調(diào)用自身缤苫,相當(dāng)于遞歸,把xhr返回的blob數(shù)據(jù)生成對應(yīng)的文件
                  download(e.target.response, fileName, defaultMime);
                };
                //  發(fā)送ajax請求
                setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
                return ajax;
            } // end if valid url?
        } // end if url?


        //go ahead and download dataURLs right away
        //  如果是dataUrl,則生成文件
        if(/^data\:[\w+\-]+\/[\w+\-]+[,;]/.test(payload)){
            //  如果滿足條件(大于2m,且myBlob !== toString)墅拭,直接通過dataUrlToBlob生成文件
            if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
                payload=dataUrlToBlob(payload);
                mimeType=payload.type || defaultMime;
            }else{      
                //  如果是ie,走navigator.msSaveBlob
                return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:
                    navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
                    //  否則走saver方法
                    saver(payload) ; // everyone else can save dataURLs un-processed
            }
            
        }//end if dataURL passed?

        blob = payload instanceof myBlob ?
            payload :
            new myBlob([payload], {type: mimeType}) ;

        //  根據(jù)傳入的dataurl,通過myBlob生成文件
        function dataUrlToBlob(strUrl) {
            var parts= strUrl.split(/[:;,]/),
            type= parts[1],
            decoder= parts[2] == "base64" ? atob : decodeURIComponent,
            binData= decoder( parts.pop() ),
            mx= binData.length,
            i= 0,
            uiArr= new Uint8Array(mx);

            for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);

            return new myBlob([uiArr], {type: type});
         }

        //  winMode 是否是在window上調(diào)用
        function saver(url, winMode){
            //  如果支持download標(biāo)簽活玲,通過a標(biāo)簽的download來下載
            if ('download' in anchor) { //html5 A[download]
                anchor.href = url;
                anchor.setAttribute("download", fileName);
                anchor.className = "download-js-link";
                anchor.innerHTML = "downloading...";
                anchor.style.display = "none";
                document.body.appendChild(anchor);
                setTimeout(function() {
                    //  模擬點擊下載
                    anchor.click();
                    document.body.removeChild(anchor);
                    //  如果在window下,還需要解除url跟文件的鏈接
                    if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
                }, 66);
                return true;
            }

            // handle non-a[download] safari as best we can:
            //  針對不支持download的safari瀏覽器谍婉,走window.open的降級操作舒憾,優(yōu)化體驗
            if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
                url=url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
                if(!window.open(url)){ // popup blocked, offer direct download:
                    if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
                }
                return true;
            }

            //do iframe dataURL download (old ch+FF):
            //  針對老的chrome或者firefox瀏覽器,創(chuàng)建iframe穗熬,通過設(shè)置iframe的url來達成下載的目的
            var f = document.createElement("iframe");
            document.body.appendChild(f);

            if(!winMode){ // force a mime that will download:
                url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
            }
            f.src=url;
            //  移除工具節(jié)點
            setTimeout(function(){ document.body.removeChild(f); }, 333);

        }//end saver



        //  針對ie10+ 走瀏覽器自帶的msSaveBlob
        if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
            return navigator.msSaveBlob(blob, fileName);
        }

        //  如果全局對象下支持URL方法
        if(self.URL){ // simple fast and modern way using Blob and URL:
        //  根據(jù)blob創(chuàng)建指向文件的ObjectURL
            saver(self.URL.createObjectURL(blob), true);
        }else{
            // handle non-Blob()+non-URL browsers:
            //  針對不支持Blob和URL的瀏覽器珍剑,通過給saver傳入dataUrl來保存文件
            if(typeof blob === "string" || blob.constructor===toString ){
                try{
                    return saver( "data:" +  mimeType   + ";base64,"  +  self.btoa(blob)  );
                }catch(y){
                    return saver( "data:" +  mimeType   + "," + encodeURIComponent(blob)  );
                }
            }

            // Blob but not URL support:
            //  支持Blob但是不支持URL方法的瀏覽器,通過構(gòu)造文件閱讀器來保存文件
            reader=new FileReader();
            reader.onload=function(e){
                saver(this.result);
            };
            reader.readAsDataURL(blob);
        }
        return true;
    }; /* end download() */
}));</script>

通過分析源碼我們可以發(fā)現(xiàn)死陆,download.js是之前提到的那些方法的集大成者招拙。為了解決最為讓人頭大的圖片自動打開的問題,腳本內(nèi)通過創(chuàng)建xhr請求措译,把響應(yīng)類型改成blob,讓瀏覽器無法識別從而避免直接打開别凤,之后再把下載的到的blob文件重新拼裝成我們需要的文件。針對不同瀏覽器的兼容性领虹,使用了a標(biāo)簽下載规哪,window.open等方法作為降級方案。我們先體驗下這瓶萬金油:

image

下載圖片體驗如絲般順滑塌衰。但是問題還沒完诉稍,注意官方的這句話:

// v4.1 adds url download capability via solo URL argument (same domain/CORS only)

啥意思呢?傳入url作為參數(shù)時,只支持同源的資源或者服務(wù)器配置了CORS支持跨域(因為使用了xhr請求)最疆。實測驗證下:

image

結(jié)果瀏覽器報跨域錯誤(這里解釋下杯巨,筆者把一個msi文件上傳到小程序云存儲上,獲取的下載鏈接努酸。鵝廠針對msi,exe等比較敏感的文件設(shè)置了不同的跨域規(guī)則服爷,導(dǎo)致這部分文件下載時會被攔截),針對這種情況获诈,使用構(gòu)造表單提交等方法可以獲得完美的體驗仍源,不受跨域限制。

總結(jié)

比較了以上幾種瀏覽器端文件下載方法之后舔涎,我們發(fā)現(xiàn)a標(biāo)簽下載體驗最好笼踩,針對圖片等資源,同源的情況下使用download屬性可以獲取比較好的效果亡嫌,window.open則會打開新的tab嚎于,畫面有跳躍桶至。使用構(gòu)造表單提交除了針對瀏覽器可以直接解析的文件體驗不佳之外,沒啥弊端匾旭,以上三種方法都不受跨域限制(這里的限制是說可以下載跨域資源,不考慮體驗)圃郊。download.js除了在下載跨域資源時會報跨域error外价涝,基本沒有硬傷,結(jié)合之前的任意一種方法,可以組裝出一個比較完美的解決方案持舆。這里給一個筆者項目中的例子:

async function downloadFile() {
    fileList.filter(item => chekcList.findIndex(sitem => item._id === sitem) >= 0)
    .map(item => item.downloadUrl).map(item => {
        //  download是download.js抽出來的函數(shù)
        const res = download(item);
        if (res !== true) {
            //  跨域錯誤無法捕獲色瘩,如果返回不是true的話就走另外一個方法
            //  downloadUrlFile是簡單封裝的通過提交表單下載文件的方法
            downloadUrlFile(item)
        }
    });
}

四種方法的比較:

方法 支持跨域 是否彈出tab 是否支持直接下載瀏覽器可展示的資源
a標(biāo)簽 同源可通過download屬性支持
window.open
構(gòu)造表單submit
download.js

參考資源

了解HTML/HTML5中的download屬性
download.js官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市逸寓,隨后出現(xiàn)的幾起案子居兆,更是在濱河造成了極大的恐慌,老刑警劉巖竹伸,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泥栖,死亡現(xiàn)場離奇詭異,居然都是意外死亡勋篓,警方通過查閱死者的電腦和手機吧享,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來譬嚣,“玉大人钢颂,你說我怎么就攤上這事“菀” “怎么了殊鞭?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尼桶。 經(jīng)常有香客問我操灿,道長,這世上最難降的妖魔是什么泵督? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任牲尺,我火速辦了婚禮,結(jié)果婚禮上幌蚊,老公的妹妹穿的比我還像新娘谤碳。我一直安慰自己,他們只是感情好溢豆,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布蜒简。 她就那樣靜靜地躺著,像睡著了一般漩仙。 火紅的嫁衣襯著肌膚如雪搓茬。 梳的紋絲不亂的頭發(fā)上犹赖,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音卷仑,去河邊找鬼峻村。 笑死,一個胖子當(dāng)著我的面吹牛锡凝,可吹牛的內(nèi)容都是我干的粘昨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼窜锯,長吁一口氣:“原來是場噩夢啊……” “哼张肾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起锚扎,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤吞瞪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后驾孔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芍秆,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年翠勉,在試婚紗的時候發(fā)現(xiàn)自己被綠了浪听。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡眉菱,死狀恐怖迹栓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俭缓,我是刑警寧澤克伊,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站华坦,受9級特大地震影響愿吹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜惜姐,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一犁跪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歹袁,春花似錦坷衍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至孟抗,卻和暖如春迁杨,著一層夾襖步出監(jiān)牢的瞬間钻心,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工铅协, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捷沸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓狐史,卻偏偏與公主長得像痒给,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子预皇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353