談談關於文件上傳下載那些事

前端開發中總免不了關於文件的上傳、下載需求。下面來總結一下經常使用的方法,歡迎討論和吐槽。javascript

form 表單提交

最傳統的文件上傳方法是使用form表單上傳文件的,只須要把enctype設置爲 multipart/form-data。這種方式上傳文件不須要 js ,並且沒有兼容問題,全部瀏覽器都支持,就是體驗不好,致使頁面刷新,頁面其餘數據丟失。前端

<form method="post" action="xxxxx" enctype="multipart/form-data">
  選擇文件:<input type="file" name="file" />
  <br />
  標題:<input type="text" name="title" />
  <br />
  <button type="submit">提交</button>
</form>

注意:input 必須設置 name 屬性,不然數據沒法發送vue

文件接口上傳

這種方法由服務端提供接口,設置相應的請求頭,前端提交 formData 形式的文件數據。java

<input id="uploadFile" type="file" name="file" accept="image/png,image/gif" />
  • accept:表示能夠選擇的文件 MIME 類型,多個 MIME 類型用英文逗號分開
  • multiple:是否能夠選擇多個文件
$('#uploadFile').on('change', function (e) {
  var file = this.files[0]

  var formData = new FormData()
  formData.append('file', file)

  $.ajax({
    url: 'xxxx',
    type: 'post',
    data: formData,
    cache: false,
    contentType: false,
    processData: false,
    success: function (res) {
      //
    },
  })
})
  • processData 設置爲 false。由於 data 值是 FormData 對象,不須要對數據作處理。
  • cache 設置爲 false,上傳文件不須要緩存。
  • contentType 設置爲 false。

分片上傳

有時候咱們上傳的文件可能很大,好比視頻等可能達到 2 個 G,這樣會形成上傳速度太慢,甚至有時候會出現連接超時的狀況。並且有時候服務端會設置文件容許上傳的大小,太大的文件就不容許上傳了。爲解決這個問題,咱們能夠將文件進行分片上傳,每次只上傳很小的一部分 好比 1M。webpack

思路

  1. 將文件按必定大小(好比 1M)截取成一小份,並將切片帶上 hash 值,用於做爲標識。
  2. 將每一個切片文件併發提交到服務端,服務端保存每一個切片文件的信息。
  3. 切片上傳完成後,服務端根據文件標識進行合併,合併完後刪除切片文件。

這樣由於每一個切片是併發上傳的,因此能夠有效地下降上傳時間。下面說一下具體的實現步驟。(PS:這是我司的實現方式,並非惟一方法,且涉及到具體接口的代碼就不貼在這裏了)ios

生成 hash 值

不管上傳文件信息仍是上傳切片文件,都必需要生成文件和切片的 hash。最簡單粗暴的 hash 值能夠用文件名字+下標來標識,可是這樣文件名一旦修改就失去了效果,而事實上只要文件內容不變,hash 就不該該變化,因此正確的作法是根據文件內容生成 hash。我司用的是 spark-md5 庫,在這裏就不一一細說了。web

文件信息上傳

在文件分片上傳以前須要把整個文件的信息如該文件的總的文件大小、文件名、哈希值等等,主要目的是初始化一個文件分片上傳事件,返回文件 id,用於每一個分片的提交。ajax

getFileId (file) {
  let vm = this
  let formData = new FormData()
  formData.append('file', file)
  axios({
    timeout: 5 * 60 * 1000,
    headers: {
      'Content-Type': 'application/json-',
      'x-data': JSON.stringify({
        fileName: file.fileName,
        size: file.size,
        hash: 'hashxxx',
      }),
    },
    url: 'xxxxxx',
    method: 'POST',
  })
  .then((res) => {
    if (res.code === '200') {
      return res.data.fileId
    })
  .catch((err) => {
    console.log(err)
  })
}

文件切片分割

當前端獲取到本地圖片後,利用 Blob.prototype.slice 方法(和數組的 slice 方法類似),將大文件按照沒小片 1M 進行切割,返回原文件的某個切片,再併發將各個分片上傳到服務端。算法

getCkunk (file, fileId) {
  let vm = this
  let chunkSize = 1024 * 1024
  let totalSize = file.size
  let count = Math.ceil(totalSize / chunkSize)
  let chunkArr = []
  for (let i = 0; i < count; i++) {
    if (i === count.length - 1) {
      chunkArr.push(file.slice(i * chunkSize, totalSize))
    } else {
      chunkArr.push(file.slice(i * chunkSize, (i + 1) * chunkSize))
    }

  for (let index = 0; index < count; index++) {
    let item = chunkArr[index]
    this.uploadChunk(item, index, fileId)
  }
}

各個分片上傳到服務端的方法。此處省略 hash 值得獲取方式。npm

ploadChunk(item, index, fileId) {
   let formData = new FormData()
   formData.append('file', item)
   request({
     headers: {
       'Content-Type': 'application/octet-stream;',
       'x-data': JSON.stringify({
         fileId: fileId,
         partId: index + 1,
         hash: res,
       })
     },
     url: 'xxxxx',
     method: 'POST',
     data: formData,
   })
   .then((res) => {
     return res.data.path
   })
   .catch((err) => {
     console.log(err)
   })
 }

顯示上傳進度條

因爲文件比較大,即便是採用分片上傳的方式也是須要必定的時間的,爲了更好的用戶體驗,前端最好是提示上傳的進度。這時候就須要後端在每一個分片的放回結果加上上傳的 100%字段。前端獲取到返回值就改變當前進度。

當最後一個分片上傳完成後,服務端返回文件的 url,前端獲取 url,同時將進度條狀態改變爲 100%。

斷點續傳

上面說到的分片上傳,解決了大文件上傳超時和服務器的限制。可是對於更大的文件,上傳並非短期內就上傳完成,甚至有時候會面臨斷網或者手動暫停,難道就要從新將整個文件上傳了,咱們固然不但願。這時候斷點續傳就派上用場了。

下面說一下實現思路。
首先斷點續傳必須是基於分片上傳的基礎上的

  1. 每一個分片上傳的時候,服務端記錄上傳好的文件 hash 值,上傳成功後返回 hash 值給前端,前端記錄 hash 值
  2. 從新上傳時,將每一個文件的 hash 值與記錄的 hash 值作比對,若是相同的話則跳過,繼續下一個分段的上傳。
  3. 所有分片上傳完成後,服務端根據文件標識進行合併,合併完後刪除小文件。

文件下載

文件下載有如下幾種方法

form 表單提交

這是最原始的方法,爲一個下載按鈕添加 click 事件,點擊時動態生成一個表單,利用表單提交的功能來實現文件的下載(實際上表單的提交就是發送一個請求)。

function downloadFile(downloadUrl, fileName) {
  // 建立表單
  let form = document.createElement('form')
  form.method = 'get'
  form.action = downloadUrl
  //form.target = '_blank';    // form新開頁面
  document.body.appendChild(form)
  form.submit()
  document.body.removeChild(form)
}
  • 優勢:兼容性好,不會出現 URL 長度限制問題。
  • 缺點:沒法知道下載的進度,沒法直接下載瀏覽器可直接預覽的文件類型(如 txt/png 等)

window.open 或 window.location.href

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

window.open('downloadFile.zip')
location.href = 'downloadFile.zip'

缺點

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

a 標籤 download 屬性

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

<a href="xxxx" download>點擊下載</a>
<!-- 重命名下載文件 -->
<a href="xxxx" download="test">點擊下載</a>

優勢:能解決不能直接下載瀏覽器可瀏覽的文件。

缺點

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

利用 Blob 對象

此方法除了能利用已知文件地址路徑進行下載外,還能經過發送 ajax 請求 api 獲取文件流進行下載。利用 Blob 對象能夠將文件流轉化成 Blob 二進制對象。

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

downdFile (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 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);
    }
  }
}

推薦文章

你也許不知道的javascript高級函數
總結javascript處理異步的方法
總結移動端H5開發經常使用技巧(乾貨滿滿哦!)
從零開始構建一個webpack項目
總結幾個webpack打包優化的方法
總結前端性能優化的方法
總結vue知識體系之高級應用篇
總結vue知識體系之實用技巧
幾種常見的JS遞歸算法
封裝一個toast和dialog組件併發布到npm

關注的個人公衆號不按期分享前端知識,與您一塊兒進步!

相關文章
相關標籤/搜索