文件上傳是 Web 開發常見需求,上傳文件須要用到文件輸入框,若是給文件輸入框添加一個 multiple
屬性則能夠一次選擇多個文件(不支持的瀏覽器會自動忽略這個屬性)javascript
<input multiple type="file">
點擊這個輸入框就能夠打開瀏覽文件對話框選擇文件了,通常一個輸入框上傳一個文件就行,要上傳多個文件也能夠用多個輸入框來處理,這樣作是爲了兼容那些不支持 multiple 屬性的瀏覽器,同時用戶通常也不會選擇多個文件html
當把文件輸入框放入表單中,提交表單的時候便可將選中的文件一塊兒提交上傳到服務器,須要注意的是因爲提交的表單中包含文件,所以要修改一下表單元素的 enctype
屬性爲 multipart/form-data
java
<form action="#" enctype="multipart/form-data" method="post"> <input name="file" type="file"> <button type="submit">Upload</button> </form>
這樣上傳方式是傳統的同步上傳,上傳的文件若是很大,每每須要等待好久,上傳完成後頁面還會從新加載,而且必須等待上傳完成後才能繼續操做web
早期的瀏覽器並不支持異步上傳,不過可使用 iframe 來模擬,在頁面中隱藏一個 <iframe>
元素,指定一個 name
值,同時將 <form>
元素的 target
屬性值指定爲 <iframe>
元素的 name
屬性的值,將二者關聯起來ajax
<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 本來就是隱藏的,即便從新加載也不會感知到api
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 的瀏覽器能夠參考 caniusepromise
因爲能夠經過 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 方便得多