前端下載文件的5種方法的對比(附加獲取文件名

前言

在前端站點上下載文件,這是一個極其廣泛的需求,很早前就已經有各類解決方法了,爲何還寫這麼老的文章,只是最近在帶一個新人,他彷佛不少都只知其一;不知其二,也遇到了咱們必經問題之「不能下載txt、png等文件」的典型問題,我就給他總結下下載的幾個方式。順便分享出來,也許,真有人須要。前端

form表單提交

這是之前常使用的傳統方式,畢竟那個年代,沒那麼多好用的新特性呀。ajax

道理也很簡單,爲一個下載按鈕添加click事件,點擊時動態生成一個表單,利用表單提交的功能來實現文件的下載(實際上表單的提交就是發送一個請求)後端

來看下如何生成一個表單,生成怎麼樣的一個表單:api

/** * 下載文件 * @param {String} path - 請求的地址 * @param {String} fileName - 文件名 */
function downloadFile (downloadUrl, fileName) {
    // 建立表單
    const formObj = document.createElement('form');
    formObj.action = downloadUrl;
    formObj.method = 'get';
    formObj.style.display = 'none';
    // 建立input,主要是起傳參做用
    const formItem = document.createElement('input');
    formItem.value = fileName; // 傳參的值
    formItem.name = 'fileName'; // 傳參的字段名
    // 插入到網頁中
    formObj.appendChild(formItem);
    document.body.appendChild(formObj);
    formObj.submit(); // 發送請求
    document.body.removeChild(formObj); // 發送完清除掉
}
複製代碼

優勢

  • 傳統方式,兼容性好,不會出現URL長度限制問題

缺點

  • 沒法知道下載的進度
  • 沒法直接下載瀏覽器可直接預覽的文件類型(如txt/png等)

open或location.href

最簡單最直接的方式,實際上跟a標籤訪問下載連接同樣跨域

window.open('downloadFile.zip');

location.href = 'downloadFile.zip';
複製代碼

固然地址也能夠是接口api的地址,而不單純是個連接地址。瀏覽器

優勢

  • 簡單方便直接

缺點

  • 會出現URL長度限制問題
  • 須要注意url編碼問題
  • 瀏覽器可直接瀏覽的文件類型是不提供下載的,如txt、png、jpg、gif等
  • 不能添加header,也就不能進行鑑權
  • 沒法知道下載的進度

a標籤的download

咱們知道,a標籤能夠訪問下載文件的地址,瀏覽器幫助進行下載。可是對於瀏覽器支持直接瀏覽的txt、png、jpg、gif等文件,是不提供直接下載(可右擊從菜單裏另存爲)的。bash

爲了解決這個直接瀏覽不下載的問題,能夠利用download屬性。微信

download屬性是HTML5新增的屬性,兼容性能夠了解下 can i use downloadmarkdown

整體兼容性算是很好了,基本能夠區分爲IE和其餘瀏覽。可是須要注意一些信息:app

  • Edge 13在嘗試下載data url連接時會崩潰。
  • Chrome 65及以上版本只支持同源下載連接。
  • Firefox只支持同源下載連接。

基於上面描述,若是你嘗試下載跨域連接,那麼其實download的效果就會沒了,跟不設置download表現一致。即瀏覽器能預覽的仍是會預覽,而不是下載。

簡單用法:

<a href="example.jpg" download>點擊下載</a>
複製代碼

能夠帶上屬性值,指定下載的文件名,即重命名下載文件。不設置的話默認是文件本來名。

<a href="example.jpg" download="test">點擊下載</a>
複製代碼

如上,會下載了一個名叫test的圖片

監測是否支持download

要知道瀏覽器是否支持download屬性,簡單的一句代碼便可區分

const isSupport = 'download' in document.createElement('a');
複製代碼

對於在跨域下不能下載可瀏覽的文件,其實能夠跟後端協商好,在後端層作多一層轉發,最終返回給前端的文件連接跟下載頁同域就行了。

優勢

  • 能解決不能直接下載瀏覽器可瀏覽的文件

缺點

  • 得已知下載文件地址
  • 不能下載跨域下的瀏覽器可瀏覽的文件
  • 有兼容性問題,特別是IE
  • 不能進行鑑權

利用Blob對象

該方法較上面的直接使用a標籤download這種方法的優點在於,它除了能利用已知文件地址路徑進行下載外,還能經過發送ajax請求api獲取文件流進行下載。畢竟有些時候,後端不會直接提供一個下載地址給你直接訪問,而是要調取api。

利用Blob對象能夠將文件流轉化成Blob二進制對象。該對象兼容性良好,須要注意的是

  • IE10如下不支持。
  • 在Safari瀏覽器上訪問Blob UrlObject URL當前是有缺陷的,以下文中經過URL.createObjectURL生成的連接。caniuse官網有指出

Safari has a serious issue with blobs that are of the type application/octet-stream

進行下載的思路很簡單:發請求獲取二進制數據,轉化爲Blob對象,利用URL.createObjectUrl生成url地址,賦值在a標籤的href屬性上,結合download進行下載。

/** * 下載文件 * @param {String} path - 下載地址/下載請求地址。 * @param {String} name - 下載文件的名字/重命名(考慮到兼容性問題,最好加上後綴名) */
downloadFile (path, name) {
    const xhr = new XMLHttpRequest();
    xhr.open('get', path);
    xhr.responseType = 'blob';
    xhr.send();
    xhr.onload = function () {
        if (this.status === 200 || this.status === 304) {
            // 若是是IE10及以上,不支持download屬性,採用msSaveOrOpenBlob方法,可是IE10如下也不支持msSaveOrOpenBlob
            if ('msSaveOrOpenBlob' in navigator) {
                navigator.msSaveOrOpenBlob(this.response, name);
                return;
            }
            // const blob = new Blob([this.response], { type: xhr.getResponseHeader('Content-Type') });
            // const url = URL.createObjectURL(blob);
            const url = URL.createObjectURL(this.response);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = name;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
    };
}
複製代碼

該方法不能缺乏a標籤的download屬性的設置。由於發請求時已設置返回數據類型爲Blob類型(xhr.responseType = 'blob'),因此target.response就是一個Blob對象,打印出來會看到兩個屬性sizetype。雖然type屬性已指定了文件的類型,可是爲了穩妥起見,仍是在download屬性值裏指定後綴名,如Firefox不指定下載下來的文件就會不識別類型。

你們可能會注意到,上述代碼有兩處註釋,其實除了上述的寫法外,還有另外一個寫法,改動一丟丟。若是發送請求時不設置xhr.responseType = 'blob',默認ajax請求會返回DOMString類型的數據,即字符串。這時就須要兩處註釋的代碼了,對返回的文本轉化爲Blob對象,而後建立blob url,此時須要註釋掉本來的const url = URL.createObjectURL(target.response)

優勢

  • 能解決不能直接下載瀏覽器可瀏覽的文件
  • 可設置header,也就可添加鑑權信息

缺點

  • 兼容性問題,IE10如下不可用;Safari瀏覽器能夠留意下使用狀況

利用base64

這裏的用法跟上面用Blob大同小異,基本上思路是同樣的,惟一不一樣的是,上面是利用Blob對象生成Blob URL,而這裏則是生成Data URL,所謂Data URL,就是base64編碼後的url形式。

/** * 下載文件 * @param {String} path - 下載地址/下載請求地址。 * @param {String} name - 下載文件的名字(考慮到兼容性問題,最好加上後綴名) */
downloadFile (path, name) {
    const xhr = new XMLHttpRequest();
    xhr.open('get', path);
    xhr.responseType = 'blob';
    xhr.send();
    xhr.onload = function () {
        if (this.status === 200 || this.status === 304) {
            const fileReader = new FileReader();
            fileReader.readAsDataURL(this.response);
            fileReader.onload = function () {
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = this.result;
                a.download = name;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
            };
        }
    };
}
複製代碼

優勢

  • 能解決不能直接下載瀏覽器可瀏覽的文件
  • 可設置header,也就可添加鑑權信息

缺點

  • 兼容性問題,IE10如下不可用

關於文件名

有時候咱們在發送下載請求以前,並不知道文件名,或者文件名是後端提供的,咱們就要想辦法獲取。

Content-Disposition

當返回文件流的時候,咱們在瀏覽器上觀察接口返回的信息,會看到有這麼一個header:Content-Disposition

Content-Disposition: attachment; filename=CMCoWork__________20200323151823_190342.xlsx; filename*=UTF-8''CMCoWork_%E4
複製代碼

上面的值是例子。

其中包含了文件名,咱們能夠想辦法獲取其中的文件名。咱們看到,有filename=filename*=,後者不必定有,在舊版瀏覽器中或個別瀏覽器中,會不支持這種形式,filename*採用了RFC 5987中規定的編碼方式。

因此你要獲取文件名,就變成,截取這段字符串中的這兩個字段值了。

看上面的例子你們可能發現,怎麼值怪怪的。是的,若是名字是英文,那好辦, 若是是有中文或者其餘特殊符號,是須要處理好編碼的

  • filename,須要後端處理好編碼形式,可是就算後端處理好了,也會應每一個瀏覽器的不一樣,解析的狀況也不一樣。是個比較難處理好的傢伙,因此纔有後面的filename*
  • filename*,是個現代瀏覽器支持的,爲了解決filename的不足,通常是UTF-8,咱們用decodeURIComponent就能解碼了,能還原成本來的樣子。固然,解碼前你要把值中的UTF-8''這種部分給去掉。

因此,在咱們實現以前,咱們就要明白,取Content-Disposition的內容,並非百分百能符合你預期的,除非你的文件名全是英文數字。

咱們提取文件名值:

// xhr是XMLHttpRequest對象
const content = xhr.getResponseHeader('content-disposition'); // 注意是全小寫,自定義的header也是全小寫
if (content) {
    let name1 = content.match(/filename=(.*);/)[1]; // 獲取filename的值
    let name2 = content.match(/filename\*=(.*)/)[1]; // 獲取filename*的值
    name1 = decodeURIComponent(name1);
    name2 = decodeURIComponent(name2.substring(6)); // 這個下標6就是UTF-8''
}
複製代碼

上面咱們得到了兩個文件名name1,name2,若是兩個都存在,那麼咱們優先取name2的,由於這個更靠譜,name1若是包含中文或特殊符號,就有風險還原不了真正的文件名。

缺陷

  • 非全數字英文的文件名,若是瀏覽器只支持filename,獲取的文件名編碼可能會有問題。

自定義header

本質上跟上述的Content-Disposition差很少,只是咱們這裏不使用默認的header,咱們本身自定義一個response header,跟後端決定好編碼方式返回,前端直接獲取這個自定義header,而後使用對應的解碼便可,如使用decodeURIComponent

可是咱們都要知道,在跨域的狀況下,前端獲取到的header只有默認的6個基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

因此你想要獲取到別的header,須要後端配合,設置

Access-Control-Expose-Headers: Content-Disposition, custom-header
複製代碼

這樣,前端就能獲取到對應暴露的header字段,須要注意的是,Content-Disposition也是須要暴露的。

重命名

這裏額外提供個方法,該方法做用是,當你知道文件的全名(含後綴名),想要重命名,可是得後綴名同樣,來獲取後綴名。

function findType (name) {
    const index = name.lastIndexOf('.');
    return name.substring(index + 1);
}
複製代碼

未經容許,請勿私自轉載

個人微信公衆號

相關文章
相關標籤/搜索