這應(yīng)該是你見(jiàn)過(guò)的最全前端下載總結(jié)

這應(yīng)該是你見(jiàn)過(guò)的最全前端下載總結(jié)

自己整理的一些項(xiàng)目中遇到過(guò)的關(guān)于上傳和下載的一些Demo,大前端系列(也就是純前端 + node端完成的下載,只要獲取到數(shù)據(jù)下載工作全是前端來(lái)做),僅供給位看官參考,避免踩坑,即插即用,歡迎fork和star??,為這個(gè)倉(cāng)庫(kù)添磚加瓦~(P.S. 個(gè)人認(rèn)為如果沒(méi)寫(xiě)過(guò)上傳下載其實(shí)還是挺麻煩的,這個(gè)基本能覆蓋大部分場(chǎng)景了~)

包括內(nèi)容

  • 純前端下載
    • 基于a標(biāo)簽
    • location && iframe
    • FileSaver ---> [推薦]
  • node端下載
    • 先下載到本地,再下載到瀏覽器
    • 直接流向?yàn)g覽器下載 [推薦]

在這里怕大家沒(méi)有耐心看下去,放一個(gè)強(qiáng)烈推薦的filesaver的Demo動(dòng)態(tài)圖

image

寫(xiě)在前面

上傳和下載個(gè)人認(rèn)為在前端開(kāi)發(fā)時(shí)稍微復(fù)雜一丟丟,需要額外處理一些事情而不是直接獲取數(shù)據(jù)渲染頁(yè)面,所以想著把平時(shí)遇到過(guò)的一些場(chǎng)景整理一下分享出來(lái),大牛繞過(guò),不喜勿噴~我平時(shí)在項(xiàng)目中接觸的也就是一些上傳圖片,上傳安裝包,下載圖片,下載安裝包以及整理數(shù)據(jù)生成excel文件下載下來(lái)。暫時(shí)還沒(méi)有接觸過(guò)其他類(lèi)型的,所以本項(xiàng)目可能有一定的局限性,只是給大家提供一種思路或者方案,有其他想法歡迎評(píng)論~

純前端下載形式

顧名思義,純前端實(shí)現(xiàn),也就是不依賴于任何后端。不過(guò)這種方式有一定的局限性,比如下載類(lèi)型,寫(xiě)法,數(shù)據(jù)形式等等。但是既然不依賴與后端,在可接受范圍之內(nèi)還是很推薦使用的,畢竟簡(jiǎn)單啊~

基于a標(biāo)簽下載

說(shuō)到簡(jiǎn)單,那么最簡(jiǎn)單的就是這個(gè)了。那就是基于<a>標(biāo)簽的下載文件方式,真的是超級(jí)簡(jiǎn)單。使用方法如下:

href: 文件的絕對(duì)/相對(duì)地址
download: 文件名(可省略,省略后瀏覽器自動(dòng)識(shí)別源文件名)
<a href='xxx.jpg' download='file.jpg'>下載jpg圖片</a>

那么既然這么簡(jiǎn)單,那肯定是存在問(wèn)題的。

image

上面這張圖片是官方提供的兼容性,目前只有FireFox和Chrome支持download屬性。至少這兩個(gè)對(duì)于開(kāi)發(fā)者來(lái)說(shuō)不陌生,占有量也很大,所以也還可以吧,但是接下來(lái)我又嘗試了一下這兩個(gè)瀏覽器的兼容性情況。


image

上面這張,是FireFox瀏覽器最新版,可以看到點(diǎn)擊下載文件會(huì)彈出一個(gè)對(duì)話框,之后點(diǎn)擊保存文件才可以進(jìn)行下載,同時(shí)只能下載不能被瀏覽器打開(kāi)的文件類(lèi)型,如圖片、文本文件、html文件這種可以被打開(kāi)的文件,是無(wú)法被下載的直接在瀏覽器進(jìn)行預(yù)覽。


image

上面這張,是Chrome最新版,與FireFox相同,對(duì)于圖片文件和文本文件這種可以被瀏覽器打開(kāi)的文件不會(huì)被下載,而excel和安裝包這種文件是可以被直接下載的,無(wú)需進(jìn)行任何二次確認(rèn)操作。

那么能不能不讓瀏覽器預(yù)覽圖片(或pdf或txt文件)?

肯定能啊~為什么呢?其實(shí)a標(biāo)簽的href屬性還可以接受除了相對(duì)和絕對(duì)路徑之外的其他形式Url,比如下面我們要用到的DataUrl和BlobUrl。我們使用這種形式,就可以讓瀏覽器不預(yù)覽而直接下載圖片了,當(dāng)然了操作起來(lái)更麻煩一些了就。

  • DataUrl
 // 首先,圖片轉(zhuǎn)base64
 // ./util.js
 
 // html頁(yè)面,將a標(biāo)簽href屬性動(dòng)態(tài)賦值為dataUrl
 <a id='downloadDataUrl' class="button is-dark">下載data:Url圖片</a>
 ...
 <script>
  const image = new Image();
  image.setAttribute("crossOrigin",'Anonymous');
  image.src = '../files/test-download.png' + '?' + new Date().getTime();
  image.onload = function() {  
    const imageDataUrl = image2base64(image);  
    const downloadDataUrlDom = document.getElementById('downloadDataUrl');
    downloadDataUrlDom.setAttribute('href', imageDataUrl);
    downloadDataUrlDom.setAttribute('download', 'download-data-url.png');
    downloadDataUrlDom.addEventListener('click', () => {
      console.log('下載文件');
    });
  }  
</script>

如下圖,可以看到不再是預(yù)覽文件,而是直接下載文件了。這里面有一些坑,比如canvas.toDataUrl的一些問(wèn)題以及解決辦法,我就不多說(shuō)了,大家自己去看看。

image

  • BlobUrl
    整體邏輯更復(fù)雜了,首先 文件 -> base64(dataUrl) -> blob -> blobUrl
 // 第一步:首先需要將文件轉(zhuǎn)換成base64,方法上面一樣
 // 第二步:將base64轉(zhuǎn)換成blob數(shù)據(jù)
 // DataUrl 轉(zhuǎn) Blob數(shù)據(jù)
    function dataUrl2Blob(dataUrl) {
      var arr = dataUrl.split(','),
          mime = arr[0].match(/:(.*?);/)[1],
          bStr = atob(arr[1]),
          n = bStr.length,
          unit8Array = new Uint8Array(n);
      while (n--) {
        unit8Array[n] = bStr.charCodeAt(n);
      }
      return new Blob([unit8Array], { type: mime });
    } 
 // 第三步: 將blob數(shù)據(jù)轉(zhuǎn)換成BlobUrl
 URL.createObjectURL(imageBlobData);
 
 // 完整代碼
  <a id='downloadBlobUrl' class="button is-danger">下載blobUrl圖片</a>
  ...
  const image2 = new Image();  
  image2.setAttribute("crossOrigin",'Anonymous');
  image2.src = '../files/test-download.png' + '?' + new Date().getTime();
  image2.onload = function() {  
    const imageDataUrl = image2base64(image2);
    const imageBlobData = dataUrl2Blob(imageDataUrl);
    const downloadDataUrlDom = document.getElementById('downloadBlobUrl');
    downloadDataUrlDom.setAttribute('href', URL.createObjectURL(imageBlobData));
    downloadDataUrlDom.setAttribute('download', 'download-data-url.png');
    downloadDataUrlDom.addEventListener('click', () => {
      console.log('下載文件');
    });
  }

【總結(jié)】: Chrome在兼容性上更勝一籌,但是二者總體來(lái)說(shuō)都存在一些問(wèn)題,不能直接下載圖片和文本文件,但是畢竟這么簡(jiǎn)潔,你沒(méi)進(jìn)行任何多余的操作,存在問(wèn)題合情合理。同時(shí),上面的幾種方式也看到了,dataUrl適合圖片的下載,而blobUrl雖然要麻煩一些,但是對(duì)于文本文件的下載還是非常有用的,你可以直接把要下載的內(nèi)容轉(zhuǎn)換成blob數(shù)據(jù),然后轉(zhuǎn)換成blobUrl進(jìn)行下載,適用于.txt,.json等文件類(lèi)型。

【建議】: 如果下載的需求是特殊文件類(lèi)型,如安裝包,excel文件,并且可以存放在CDN又一個(gè)可訪問(wèn)的url鏈接。那么這種方式非常完美,當(dāng)然,如果你可以接受上面所說(shuō)的兼容性問(wèn)題。同時(shí)如果你采用dataUrl或者blobUrl的時(shí)候,由于存在很多問(wèn)題,比如cors之類(lèi)的事情,建議可以使用這種方法,但是需要配合后端,也就是后端幫你轉(zhuǎn)換好,你直接拿轉(zhuǎn)換好的url來(lái)下載就行了。

location.href 和 iframe下載

上面這兩種非常好理解,就是在另一個(gè)窗口或者當(dāng)前地址欄地址指向下載鏈接,下載鏈接要求是dataUrl或者blobUrl。只不過(guò),iframe是更高級(jí)一些,也就是可以幫助我們做到無(wú)閃下載,作為開(kāi)發(fā)者大家應(yīng)該都懂,我就不多BB了。

image

從上面這個(gè)動(dòng)圖,可以看出來(lái),這個(gè)方法其實(shí)還不如<a>標(biāo)簽下載,為什么這么說(shuō)呢,會(huì)因?yàn)閍標(biāo)簽方法雖然會(huì)預(yù)覽瀏覽器可以預(yù)覽的文件,但是如果進(jìn)行適當(dāng)轉(zhuǎn)化,還是能進(jìn)行下載的。但是location這種方法無(wú)論是dataUrl還是blobUrl,只要是圖片、文本文件以及pdf等所有瀏覽器可以打開(kāi)的文件,都會(huì)直接給你預(yù)覽,只能下載那些瀏覽器不支持預(yù)覽的那些文件。所以Just so so了。

iframe封裝無(wú)閃現(xiàn)下載方法

本質(zhì)很簡(jiǎn)單,就是不讓當(dāng)前瀏覽器窗口執(zhí)行下載操作,而是另開(kāi)一個(gè)iframe進(jìn)行文件的下載。但是這個(gè)iframe是用戶不可見(jiàn)的~
這里需要注意,如果是純前端,建議不要進(jìn)行圖片等瀏覽器可打開(kāi)的文件下載,因?yàn)殡[藏iframe里打開(kāi)你也看不到,也就是他的問(wèn)題還是上面那些??梢赃M(jìn)行excel、zip以及各種資源文件的下載。

// 無(wú)閃現(xiàn)下載excel
function download(url) {
  const iframe = document.createElement('iframe');
  iframe.style.display = 'none';
  function iframeLoad() {
    console.log('iframe onload');
    const win = iframe.contentWindow;
    const doc = win.document;
    if (win.location.href === url) {
      if (doc.body.childNodes.length > 0) {
        // response is error
      }
      iframe.parentNode.removeChild(iframe);
    }
  }
  if ('onload' in iframe) {
    iframe.onload = iframeLoad;
  } else if (iframe.attachEvent) {
    iframe.attachEvent('onload', iframeLoad);
  } else {
    iframe.onreadystatechange = function onreadystatechange() {
      if (iframe.readyState === 'complete') {
        iframeLoad;
      }
    };
  }
  iframe.src = '';
  document.body.appendChild(iframe);

  setTimeout(function loadUrl() {
    iframe.contentWindow.location.href = url;
  }, 50);
}

如果你在項(xiàng)目里需要進(jìn)行無(wú)閃現(xiàn)下載,什么都不用做,只需要調(diào)用 download(url),即可進(jìn)行無(wú)閃現(xiàn)下載~親測(cè)可用

使用FileSaver強(qiáng)大的前端下載插件 -> 【強(qiáng)烈推薦】

FileSaver的下載方式完全是前端(Client-Side)的下載方式,它是基于Blob進(jìn)行下載的,當(dāng)然因?yàn)槭腔谇岸讼螺d,所以瀏覽器下載會(huì)有一定的限制,也就是Blob數(shù)據(jù)的大小不能過(guò)大,看看官網(wǎng)給的相關(guān)參數(shù):

image

可以看到,基本對(duì)于支持的瀏覽器來(lái)說(shuō),大小可以達(dá)到 500MB+,應(yīng)該已經(jīng)可以滿足大部分需求了。如果文件確實(shí)很大,官網(wǎng)給出了替代方案StreamSaver,沒(méi)去研究過(guò)這個(gè),不過(guò)作者既然推薦可能也很好,感興趣的可以去看看。

前面講到了,F(xiàn)ileSaver是基于Blob的,其實(shí)并不準(zhǔn)確,可以看一下官網(wǎng):

image

其實(shí)它支持Blob、File和Url進(jìn)行下載,但是如果基于url了我也沒(méi)必要用FileSaver了,那個(gè)<a>標(biāo)簽也挺好的是不?然后基于File一般都是特定場(chǎng)景,比如上傳的時(shí)候,才會(huì)用到FileReader之類(lèi)的API,說(shuō)實(shí)話我也沒(méi)怎么用過(guò),都是封裝的,所以這里也不做介紹。開(kāi)頭也說(shuō)過(guò)了,希望小伙伴可以給這個(gè)倉(cāng)庫(kù)添加?xùn)|西啊,可以增加自己的下載Demo到這里,非常歡迎

所以我這篇文章討論的下載,就是基于Blob。首要工作就是將文件轉(zhuǎn)換成Blob數(shù)據(jù)。下面幾個(gè)例子都是這樣:

FileSaver ---- 下載canvas

這個(gè)Demo簡(jiǎn)單點(diǎn)的話其實(shí)可以直接用canvas畫(huà)一個(gè)image在頁(yè)面上,然后再進(jìn)行下載,但是那樣還不如直接下載圖片了,所以麻煩一些,寫(xiě)一個(gè)canvas白板,然后下載我們自己繪制的內(nèi)容并且起名字進(jìn)行下載。

image
 // 生成下載的文件名 
 function generateFilename(id, mime) {
    const filename = document.getElementById(id).value || document.getElementById(id).placeholder;
    return filename + mime;
  }
  const canvasDownloadDom = document.getElementById('download-canvas');
  canvasDownloadDom.addEventListener('click', () => {
    const canvas = document.getElementById('canvas');
    const filename = generateFilename('canvasName', '.png');
    if (canvas.toBlob) {
      // 調(diào)用方法將canvas轉(zhuǎn)換成blob數(shù)據(jù)
      canvas.toBlob(
          function (blob) {
            // 調(diào)用FileSaver方法下載
            saveAs(blob, filename);
          },
          'image/png'
      );
    }   
  });

代碼非常簡(jiǎn)單,感興趣的小伙伴可以去看看每個(gè)插件內(nèi)部的代碼。我這里就是應(yīng)用級(jí)別的示例了。

FileSaver ---- 直接下載圖片

直接下載圖片就是將圖片轉(zhuǎn)換成Blob數(shù)據(jù),然后進(jìn)行下載。

image
// FileSaver 下載文件
  const image = new Image();  
  image.setAttribute("crossOrigin",'Anonymous');
  image.src = '../files/test-download.png' + '?' + new Date().getTime();
  image.onload = function() {  
    const imageDataUrl = image2base64(image);
    const imageBlobData = dataUrl2Blob(imageDataUrl);
    const downloadImageDom = document.getElementById('download-image');
    downloadImageDom.addEventListener('click', () => {
      saveAs(imageBlobData, 'test-download.png');
    });
  }

這代碼就更簡(jiǎn)單了,就是前面<a>標(biāo)簽下載Blob數(shù)據(jù)的代碼,數(shù)據(jù)轉(zhuǎn)換是一樣的,只不過(guò)下載使用的是FileSaver。

FileSaver ---- 下載文本文件

下載文本文件就更容易了,因?yàn)镴avaScript支持直接將字符串構(gòu)造成Blob對(duì)象。

const textBlob = new Blob(["your target string"], {type: "text/plain;charset=utf-8"});
image

下載下來(lái)的txt文件長(zhǎng)這樣:


image
 // FileSaver 下載文本文件
  const txtDownloadDom = document.getElementById('download-txt');
  txtDownloadDom.addEventListener('click', () => {
    const textarea = document.getElementById('textarea');
    const filename = generateFilename('textareaName', '.txt');
    const textBlob = new Blob([textarea.value], {type: "text/plain;charset=utf-8"});
    saveAs(textBlob, filename);
  });

FileSaver ---- 下載Excel文件(搭配js-xlsx)

前面的都相對(duì)簡(jiǎn)單一些,但是其實(shí)除了下載圖片,可能平時(shí)也沒(méi)什么業(yè)務(wù)場(chǎng)景需要到。接下來(lái)要說(shuō)的可是所有商務(wù)系統(tǒng)幾乎都能遇到的了,那就是 —— 下載報(bào)表,也就是Excel文件。這里面就使用FileSaver配合js-xlsx來(lái)進(jìn)行excel的純前端下載工作~

image

下載下來(lái)的文件長(zhǎng)這樣:


image
// 下載excel文件
  const excelDownloadDom = document.getElementById('download-excel');
  excelDownloadDom.addEventListener('click', () => {
    // 找到table節(jié)點(diǎn)調(diào)用方法轉(zhuǎn)化數(shù)據(jù)
    const wb = XLSX.utils.table_to_book(document.querySelector('#table-excel'));
    // 生成excel數(shù)據(jù)
    const wbout = XLSX.write(wb, { bookType: 'xlsx', bookSST: true, type: 'array' });
    try {
      // 下載excel文件
      saveAs(new Blob([wbout], { type: 'application/octet-stream' }), 'table-excel.xlsx');
    } catch (e) {
      if (typeof console !== 'undefined') console.log(e, wbout)
    }
  });

這里面我只是介紹如何用FileSaver在前端下載excel文件,至于js-xlsx如何將數(shù)據(jù)轉(zhuǎn)化成excel的這里不做介紹。我只是簡(jiǎn)單的調(diào)用了js-xlsx的將table轉(zhuǎn)成excel的方法, js-xlsx還有很多高級(jí)功能,有這方面需求的去看看官方文檔js-xlsx就好了~

node端配合下載(大前端)

帶后端支持的下載就要輕松很多了,為什么呢,因?yàn)樯厦嫠屑兦岸讼螺d都可以與后端進(jìn)行配合使用,也就是后端生成對(duì)應(yīng)的下載鏈接下載數(shù)據(jù)返回給前端,前端根據(jù)設(shè)計(jì)方案按需使用上面幾種方式下載,肯定能下載成功。
那么node端配合下載肯定是要下載點(diǎn)不一樣的東西了——那就是文件流。

有很多場(chǎng)景,那就是大文件不是存在于CDN,而是以文件流的形式存放在內(nèi)存。那么就沒(méi)有對(duì)應(yīng)的下載鏈接,下載對(duì)應(yīng)文件的時(shí)候,后端返回的就是文件流。而node里為我們提供Stream支持各種流操縱。所以我們可以在node端直接進(jìn)行文件的下載。

先下載到本地在從瀏覽器下載

  • fs下載Excel


    image

    下載下來(lái)的Excel文件:


    image
// 第一步:構(gòu)造數(shù)據(jù)
const data = [
  [1, 2, 3],
  [true, false, null, 'sheetjs'],
  ['foo', 'bar', new Date('2014-02-19T14:30Z'), '0.3'],
  ['baz', null, 'qux'],
];
// 第二步:生成excel的Buffer數(shù)據(jù)
const buffer = xlsx.build([{ name: 'mySheetName', data }]);

// 第三步:寫(xiě)文件到本地
const tmpExcel = `filename.xlsx`;

fs.writeFileSync(
  tmpExcel,
  buffer,
  {
    encoding: 'utf8',
  },
  err => {
    if (err) throw new Error(err);
  },
);
// 第四步:從本地讀取文件下載到瀏覽器
res.setHeader('Content-disposition', `attachment; filename="${tmpExcel}"`);
res.setHeader('Content-Type', 'application/octet-stream');
// pipe generated file to the response
fs.createReadStream(tmpExcel).pipe(res);
// 下載完成后刪除文件
fs.unlinkSync(tmpExcel);

下載excel在node端我使用的不是js-xlsx而是node-xlsx,因?yàn)樗鼧?gòu)造數(shù)據(jù)非常簡(jiǎn)單,功能也很強(qiáng)大,十分推薦大家使用~

  • fs下載文件

這里場(chǎng)景不是很容易描述,因?yàn)镈emo我都是將文件放到本地目錄的,所以我讀取本地文件再下載到本地再下載到瀏覽器,我這不是有病嗎。。。一般場(chǎng)景是文件以文件流的形式存在內(nèi)存里,然后我們通過(guò)接口下載到本地再?gòu)谋镜叵螺d到瀏覽器?;蛘呤巧蟼魑募4娴奖镜?,然后在從本地進(jìn)行相關(guān)操作,這里就不寫(xiě)示例代碼了。

node端直接流向?yàn)g覽器下載【推薦】

node端,我使用的是express框架(其他的框架也都一樣),如果你是文件流直接過(guò)來(lái)的,那么直接調(diào)用res.attachment()下載文件流,如果是文件path,那么可以直接res.download(filepath)。具體見(jiàn)demo

  • 直接下載Excel

上面過(guò)程其實(shí)多經(jīng)歷了一步,為什么呢?因?yàn)槟玫絙uffer之后我們其實(shí)就可以直接將buffer流向?yàn)g覽器下載了~這里我用的是Express框架,直接使用res.attachment()方法就可以了。

image

下載下來(lái)的文件與上面一模一樣我就不展示了。

按照我的理解,第二種明顯要比第一種好很多為什么還要列出第一種呢?我個(gè)人覺(jué)得,第一種雖然一定會(huì)犧牲一定的性能,但是先下載到本地就可以對(duì)文件進(jìn)行一些校驗(yàn),比如文件是否完整,文件名之類(lèi)的是否合法,還有些時(shí)候的場(chǎng)景可能。畢竟不是所有的下載場(chǎng)景都像Demo這樣簡(jiǎn)單。存在即合理,所以還是都羅列出來(lái)。

// 第一步:構(gòu)造數(shù)據(jù)
const data = [
  [1, 2, 3],
  [true, false, null, 'sheetjs'],
  ['foo', 'bar', new Date('2014-02-19T14:30Z'), '0.3'],
  ['baz', null, 'qux'],
];
// 第二步:生成buffer
const buffer = xlsx.build([{ name: 'mySheetName', data }]);
// 第三步:直接下載
res.status(200)
  .attachment('bufferExcel.xlsx')
  .send(buffer);
// 上面下載代碼等同于下面這段代碼(nodejs原生代碼)
res.setHeader('Content-disposition', `attachment; filename="${tmpExcel}"`);
res.setHeader('Content-Type', 'application/octet-stream');
res.end(buffer);
  • 直接下載文件流

這里我把文件安裝包放在本地了,然后我先讀取文件內(nèi)容同時(shí)下載到瀏覽器~

// 第一種,已知文件路徑直接下載
try {
  const packagePath = 'static/download/iTerm2-3_2_5.zip';
  res.download(path.join(rootDir, packagePath));
} catch (e) {
  console.error(e);
}
// 第二種,讀取本地文件流向?yàn)g覽器
res.setHeader('Content-disposition', `attachment; filename="download-package.zip"`);
res.setHeader('Content-Type', 'application/octet-stream');
fs.createReadStream(path.join(rootDir, packagePath), 'utf-8').pipe(res);

request

最后給大家安利一個(gè)將Stream API使用到極致的Http(Https)請(qǐng)求庫(kù) —— request。

// 不加這一行下載下來(lái)的文件沒(méi)有后綴
res.setHeader('Content-disposition', 'attachment; filename=node-v8.14.0-linux-x64.tar.gz');
request('https://npm.taobao.org/mirrors/node/v8.14.0/node-v8.14.0-linux-x64.tar.gz')
  .pipe(res);

文章來(lái)源:frontend-download-sample

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容