檔案下載那點事

行走的柯南發表於2019-04-14

Content-Disposition / Content-Type

Content-Disposition

http 頭部的 Content-Disposition欄位,規定了返回的內容用什麼形式展示

value 含義 是否預設
inline 以網頁或者頁面的一部分
attachment 以附件的形式下載並儲存到本地
http.createServer((req, res) => {
  res.setHeader("Content-Disposition", "attachment")
  res.end('123 - 321 - 1234567')    
})
複製程式碼

前端需要使用window.open 形式訪問 此路由就可以實現檔案的下載

window.open(xxxx)
複製程式碼

或者使用 H5新屬性 a 標籤

<a type='download' href=xxx> 點選下載 </a>
複製程式碼

Content-Type

http.createServer((req, res) => {
  res.setHeader("Content-Type", "application/octet-stream")
  res.end('123 - 321 - 1234567')    
})
複製程式碼

同上前端使用 open 或者 a標籤進行處理

備註: 如果使用普通的請求,是不可以的,比如使用ajax

window.open

window.open 可以下載檔案的原因是,瀏覽器遇到無法解析的檔案就是執行下載

當使用瀏覽器開啟檔案時, 如果它無法解析,那麼就會把該檔案下載下來

http.createServer((req, res) => {
  const data = fs.readFileSync('./Zip.zip')
  res.end(data)    
})
複製程式碼

拿到的data是一個buffer 物件,那麼直接寫一個Buffer可以實現下載麼

data = new Buffer('我是誰,誰是我')
複製程式碼

嘗試後發現,koa是可以的。但是原生直接這麼寫是不行的

http.createServer((req, res) => {
  const newBuf = new Buffer('我是誰,誰是我')
  res.end(newBuf)    
})
複製程式碼

通過介面拿到資料之後進行下載

在實際專案中,檔案下載可能出現的場景

  • 對於已經在伺服器存在的檔案進行下載,比如圖片資源

  • 將一些查詢資料匯出到本地, 比如mysql查詢結果匯出csv

對於已存在的資源,可以直接使用 上面說的 winodw.open 或者 a 標籤進行下載,那麼對於不是以檔案形式存在的資源呢

data URI

經常見到使用 data URI scheme 的是圖片, 一般為了減少http請求,會將圖片直接以base64的形式展示在html中

<img src='data:image/png;base64,xxxxxx'/>
複製程式碼

也可以應用到檔案下載中

介面

router.get('/download', async (ctx, next) => {
  const newBUf = new Buffer('我是誰,誰是我')
  ctx.body = newBUf
}
複製程式碼

前端

axios.get(`${path}`)
.then(function ({data}) {
  let a = document.createElement('a');
  a.href = "data:text/plain;charset=utf-8," + data;
  a.download = "myfilename.png";
  a.click();
})
複製程式碼

Blob

Blob 是表示一個類檔案物件,可以用它來表示一個檔案

server 部分

http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  let end = ''
  if (req.url.includes("/down")) {
    const newBUf = new Buffer('我是誰,誰是我')
    end = newBUf
  }
  res.end(end)
}  
複製程式碼

前端

    const xhr = new XMLHttpRequest();
    xhr.open('GET', path);
    xhr.responseType = 'blob';

    xhr.onload = function () {
      const blob = xhr.response;
      const url = URL.createObjectURL(blob);
      // 通過a標籤去下載
      const link = document.createElement('a');
      link.href = url;
      link.download = fileName;
      link.click();
      
      URL.revokeObjectURL(url);
    };
   xhr.send();
複製程式碼

嘗試用 window.open 方式,發現不可以

檔案下載那點事

目前常用的各個請求庫也支援返回內容為blob格式

  axios.get(`${path}`, {
    responseType: 'blob'
  })
複製程式碼

或者 fetch

  fetch(path).then(res => res.blob().then(blob =>{ ... })
複製程式碼

window.URL

  • createObjectURL

    用 blob 物件來建立一個 object URL(它是一個 DOMString),我們可以用這個 object URL 來表示某個 blob 物件,這個 object URL 可以用在 a 標籤的 href屬性上,然後觸發點選事件,就可以下載檔案了

  • revokeObjectURL

    為了避免避免記憶體洩漏,需要手動釋放建立的 object URL

缺點

  • 構建完 blob 物件後才會轉換成檔案

從程式碼就能看出,需要先將返回內容轉為blob才進行下載操作,如果使用者操作的是一個很大的資源,在等待檔案正式下載前,還需要一段時間等待格式的轉化

例項

將mysql資料 匯出 csv

這邊直接用資料模擬mysql查詢結果, 在網上查到兩種拼接方式,肯定有其餘的方案

  • 第一種
const result = [
  ['id', 'oreder', 'name', 'status' ],
  [1, '201904120201', '正在載入', '已完成'],
  [18, '201904120204', '測試189', '待付款'],
  [22, '201904120209', '藍田日暖', '待付款'],
]

// 可以看到 result 陣列的第一組是 csv的各個欄位標題 

const data = csvData.reduce((cur, next) => `${cur + next.join(',')}\n`, '')
const blob = new Blob([data]);
const url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
複製程式碼
  • 第二種
const result = [
  [1, '201904120201', '正在載入', '已完成'],
  [18, '201904120204', '測試189', '待付款'],
  [22, '201904120209', '藍田日暖', '待付款'],
]

// result 就是正常的資料格式

  // 解決亂碼問題
  let dataType = '\uFEFF'
  // 新增表格的頭子段
  dataType += (['  訂單編號', '使用者', '動態ID'].join(',')) 
  dataType += '\n'

  result.forEach(item => { 
    // dataType += ([item.id, item.order, item.name, item.staus].join(','))
    dataType += (item.join(','))
    dataType += '\n'
  })

  const blob = new Blob([dataType], {type: 'text/csv'})    
複製程式碼

如果有幾段流檔案,前端需要下載拼接成一個完整檔案,如何實現

肯定是在 server端進行檔案的拼接操作,然後返回到前端下載

大致過程

const content1 = '這裡是第1段檔案內容'

const content2 = '這裡是第2段檔案內容'

ctx.body = { code:200, data: content1 + content2 }
複製程式碼

前端就是上文中的blob 下載方式了

下載的csv開啟數字展示有問題

如圖,使用wps開啟會被展示xxxE+15

檔案下載那點事

開發時候使用Mac自帶的numbers是沒有問題的,沒有發現換一個軟體就不行了,處理方案從網路上查到的,給過長的資料新增一個 /t 就可以了 [使用 ' 半形單引號 或者 \t 都可以]

result.id = `${id}\t`
複製程式碼

批量下載如何處理

其實這個問題是在查詢資料的過程有人提到的問題

  • window.open

批量寫了多個 window.open 在Chrome中 可以正常使用,但是在safari中,只能開啟一個視窗,下載一個檔案

  • 轉為下載一個zip 檔案

感覺如果將所有檔案合併成為一個xxx.zip 然後對 這個zip檔案做下載。不過這種方式有問題,目前查到的大部分過程都是會在伺服器新建出一個 zip 檔案,等下載完畢在做刪除,還沒有找到可以跨過這一步的方式。

參考文章

相關文章