天天看點

檔案上傳

web 端上傳需要了解的

檔案上傳是 Web 開發常見需求,上傳檔案需要用到檔案輸入框,如果給檔案輸入框添加一個

multiple

屬性則可以一次選擇多個檔案(不支援的浏覽器會自動忽略這個屬性)

<input multiple type="file">
           

點選這個輸入框就可以打開浏覽檔案對話框選擇檔案了,一般一個輸入框上傳一個檔案就行,要上傳多個檔案也可以用多個輸入框來處理,這樣做是為了相容那些不支援 multiple 屬性的浏覽器,同時使用者一般也不會選擇多個檔案

基本上傳方式

當把檔案輸入框放入表單中,送出表單的時候即可将選中的檔案一起送出上傳到伺服器,需要注意的是由于送出的表單中包含檔案,是以要修改一下表單元素的

enctype

屬性為

multipart/form-data

<form action="#" enctype="multipart/form-data" method="post">
  <input name="file" type="file">
  <button type="submit">Upload</button>
</form>
           

這樣上傳方式是傳統的同步上傳,上傳的檔案如果很大,往往需要等待很久,上傳完成後頁面還會重新加載,并且必須等待上傳完成後才能繼續操作

早期的浏覽器并不支援異步上傳,不過可以使用 iframe 來模拟,在頁面中隐藏一個

<iframe>

元素,指定一個

name

值,同時将

<form>

元素的

target

屬性值指定為

<iframe>

name

屬性的值,将兩者關聯起來

<form action="#" enctype="multipart/form-data" method="post" target="upload-frame">
  <input name="file" type="file">
  <button type="submit">Upload</button>
</form>
<iframe id="upload-frame" name="upload-frame" src="about:blank" style="display: none;"></iframe>
           

這樣在送出表單上傳的時候,頁面就不會重新加載了,取而代之的是 iframe 重新加載了,不過 iframe 原本就是隐藏的,即使重新加載也不會感覺到

通路檔案

File API 提供了通路檔案的能力,通過輸入框的

files

屬性通路,這會得到一個 FileList,這是一個集合,如果隻選擇了一個檔案,那麼集合中的第一個元素就是這個檔案

var input = document.querySelector('input[type="file"]')
var file = input.files[0]

console.log(file.name) // 檔案名稱
console.log(file.size) // 檔案大小
console.log(file.type) // 檔案類型
           

支援 File API 的浏覽器可以參考 caniuse

Ajax 上傳

由于可以通過 File API 直接通路檔案内容,再結合 XMLHttpRequest 對象直接将檔案上傳,将其作為參數傳給 XMLHttpRequest 對象的 send 方法即可

var xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.send(file)
           

不過一些原因不建議直接這樣傳遞檔案,而是使用 FormData 對象來包裝需要上傳的檔案,FormData 是一個構造函數,使用的時候先 new 一個執行個體,然後通過執行個體的 append 方法向其中添加資料,直接把需要上傳的檔案添加進去

var formData = new FormData()
formData.append('file', file, file.name) // 第 3 個參數是檔案名稱
formData.append('username', 'Mary') // 還可以添加額外的參數
           

甚至也可以直接把表單元素作為執行個體化參數,這樣整個表單中的資料就全部包含進去了

var formData = new FormData(document.querySelector('form'))
           

資料準備好後,就是上傳了,同樣是作為參數傳給 XMLHttpRequest 對象的 send 方法

var xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.send(formData)
           

監測上傳進度

XMLHttpRequest 對象還提供了一個 progress 事件,基于這個事件可以知道上傳進度如何

var xhr = new XMLHttpRequest()
xhr.open('POST', '/upload/url', true)
xhr.upload.onprogress = progressHandler // 這個函數接下來定義
           

上傳的 progress 事件由 xhr.upload 對象觸發,在事件處理程式中使用這個事件對象的 loaded(已上傳位元組數) 和 total(總數) 屬性來計算上傳的進度

function progressHandler(e) {
  var percent = Math.round((e.loaded / e.total) * 100)
}
           

上面的計算會得到一個表示完成百分比的數字,不過這兩個值也不一定總會有,保險一點先判斷一下事件對象的 lengthComputable 屬性

function progressHandler(e) {
  if (e.lengthComputable) {
    var percent = Math.round((e.loaded / e.total) * 100)
  }
}
           

支援 Ajax 上傳的浏覽器可以參考 caniuse

分割上傳

使用檔案對象的 slice 方法可以分割檔案,給該方法傳遞兩個參數,一個起始位置和一個結束位置,這會傳回一個新的 Blob 對象,包含原檔案從起始位置到結束位置的那一部分(檔案 File 對象其實也是 Blob 對象,這可以通過

file instanceof Blob

确定,Blob 是 File 的父類)

var blob = file.slice(0, 1024) // 檔案從位元組位置 0 到位元組位置 1024 那 1KB
           

将檔案分割成幾個 Blob 對象分别上傳就能實作将大檔案分割上傳

function upload(file) {
  let formData = new FormData()
  formData.append('file', file)
  let xhr = new XMLHttpRequest()
  xhr.open('POST', '/upload/url', true)
  xhr.send(formData)
}

var blob = file.slice(0, 1024)
upload(blob) // 上傳第一部分

var blob2 = file.slice(1024, 2048)
upload(blob2) // 上傳第二部分

// 上傳剩餘部分
           

通常用一個循環來處理更友善

var pos = 0 // 起始位置
var size = 1024 // 塊的大小

while (pos < file.size) {
  let blob = file.slice(pos, pos + size) // 結束位置 = 起始位置 + 塊大小

  upload(blob)
  pos += size // 下次從結束位置開始繼續分割
}
           

伺服器接收到分塊檔案進行重新組裝的代碼就不在這裡展示了

使用這種方式上傳檔案會一次性發送多個 HTTP 請求,那麼如何處理這種多個請求同時發送的情況呢?方法有很多,可以用 Promise 來處理,讓每次上傳都傳回一個 promise 對象,然後用 Promise.all 方法來合并處理,Promise.all 方法接受一個數組作為參數,是以将每次上傳傳回的 promise 對象放在一個數組中

var promises = []

while (pos < file.size) {
  let blob = file.slice(pos, pos + size)

  promises.push(upload(blob)) // upload 應該傳回一個 promise
  pos += size
}
           

同時改造一下 upload 函數使其傳回一個 promise

function upload(file) {
  return new Promise((resolve, reject) => {
    let formData = new FormData()
    formData.append('file', file)
    let xhr = new XMLHttpRequest()
    xhr.open('POST', '/upload/url', true)
    xhr.onload = () => resolve(xhr.responseText)
    xhr.onerror = () => reject(xhr.statusText)
    xhr.send(formData)
  })
}
           

當一切完成後

Promise.all(promises).then((response) => {
  console.log('Upload success!')
}).catch((err) => {
  console.log(err)
})
           

支援檔案分割的浏覽器可以參考 caniuse

判斷一下檔案對象是否有該方法就能知道浏覽器是否支援該方法,對于早期的部分版本浏覽器需要加上對應的浏覽器廠商字首

var slice = file.slice || file.webkitSlice || file.mozSlice

if (slice) {
  let blob = slice.call(file, 0, 1024) // call
  upload(blob)
} else {
  upload(file) // 不支援分割就隻能直接上傳整個檔案了,或者提示檔案過大
}
           

拖拽上傳

通過拖拽 API 可以實作拖拽檔案上傳,預設情況下,拖拽一個檔案到浏覽器中,浏覽器會嘗試打開這個檔案,要使用拖拽功能需要阻止這個預設行為

document.addEventListener('dragover', function(e) {
  e.preventDefault()
  e.stopPropagation()
})
           

任意指定一個元素來作為釋放拖拽的區域,給一個元素綁定 drop 事件

var element = document.querySelector('label')
element.addEventListener('drop', function(e) {
  e.preventDefault()
  e.stopPropagation()

  // ...
})
           

通過該事件對象的 dataTransfer 屬性擷取檔案,然後上傳即可

var file = e.dataTransfer.files[0]
upload(file) // upload 函數前面已經定義
           

選擇類型

給檔案輸入框添加

accept

屬性即可指定選擇檔案的類型,比如要選擇 png 格式的圖檔,則指定其值為

image/png

,如果要允許選擇所有類型的圖檔,就是

image/*

<input accept="image/*" type="file">
           

添加

capture

屬性可以調用裝置機能,比如

capture="camera"

可以調用相機拍照,不過這并不是一個标準屬性,不同裝置實作方式也不一樣,需要注意

<input accept="image/*" capture="camera" type="file">
           

經測 iOS 裝置添加該屬性後隻能拍照而不能從相冊選擇檔案了,是以判斷一下

if (iOS) { // iOS 用 navigator.userAgent 判斷
  input.removeAttribute('capture')
}
           

不支援的浏覽器會自動忽略這些屬性

自定義樣式

檔案輸入框在各個浏覽器中呈現的樣子都不大相同,而且給 input 定義樣式也不是那麼友善,如果有需要應用自定義樣式,有一個技巧,可以用一個 label 關聯到這個檔案輸入框,當點選這個 label 元素的時候就會觸發檔案輸入框的點選,打開浏覽檔案的對話框,相當于點選了檔案輸入框一樣的效果

<label for="file-input"></label>
<input id="file-input" style="clip: rect(0,0,0,0); position: absolute;" type="file">
           

這時就可以将原本的檔案輸入框隐藏了,然後給 label 元素任意地應用樣式,畢竟要給 label 元素應用樣式比 input 友善得多

繼續閱讀