在AJAX還不流行的年代,表單上傳文件是基本操做。表單上傳文件很簡單,有兩個須要重點關注的屬性:html
屬性用於設定form表單提交的時候數據編碼方式,一共有三種參數選擇:前端
application/x-www-form-urlencoded
發送前編碼全部字符multipart/form-data
不對字符進行編碼text/plain
空格轉換爲+
,可是不會對字符進行編碼
若是想要使用文件上傳,必須指定爲第二個屬性值:enctype=multipart/form-data
html5
對於選擇文件的時候若是想對文件進行多選,那麼必需要設置<input type="file" multiple="multiple">
git
一個比較完整代碼片斷github
<form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" multiple="multiple">
<input type="submit" value="submit"/>
</form>
複製代碼
若是要實現頁面不刷新的文件上傳,有兩種經常使用的方案:web
<iframe>
表單提交方案- AJAX方案
第一種方案在頁面中嵌套一個<iframe>
,將表單放置於<iframe>
中,此時完成表單提交不會發生全局頁面刷新。可是這個方案,隨着AJAX的逐漸完善以及先後端分離和單頁面應用的普及,輪爲了很不常規的替代方案。ajax
實現AJAX上傳,首先須要對XHR有所瞭解(若有不瞭解的能夠參照MDN的學習文檔AJAX開始)算法
XHR在發送數據的時候能夠接受一個html5的新對象FormData
,能夠經過將包含文件的表單/活着將文件放到FormData
中傳遞到後端接口,編程
html:
<form id="fileForm">
<input type="file" name="file" multiple="multiple" onchange="changeFileChoose(event)">
<input type="button" onclick="upload();" value="submit"/>
</form>
js:
let formData = new FormData(document.getElementById('fileForm'));
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload');
xhr.setRequestHeader('Content-Type', 'multipart/form-data');
xhr.send(formData);
複製代碼
若是表單中每一個文件想單獨發送請求(發送屢次請求),能夠獲取表單中文件信息並構建多個表單對象上傳canvas
formData.getAll('file').filter(file => {
return file.name
}).forEach((file, index) => {
let separateFormData = new FormData();
separateFormData.set('file', file);
xhr.send(separateFormData)
})
複製代碼
PS:在傳遞到時候注意設置請求頭信息Content-type: multiple/form-data
來支持文件上傳操做
將上傳過程的上傳進度告訴用戶是一個很好的用戶交互行爲,一方面避免用戶屢次重複上傳,另外一方面也是對用戶操做對反饋,告訴用戶系統正在處理他的操做。
監聽文件上傳進度,我的認爲要麼前端輪詢獲取後端的文件寫入狀況,要麼前端有支持上傳進度獲取對事件,其實確實AJAX上傳過程當中提供了相關對象,獲取到文件的網絡傳輸狀況的,因此在對上傳結果要求並不是十分嚴格的狀況下,經過前端監聽反饋進度已經足夠了
上傳進度的監聽須要使用xhr.upload
對象的事件,利用監聽xhr.upload.onprogress
來實現上傳進度的監聽
xhr.upload.onprogress = ev => {
console.log(`upload loaded: ${ev.loaded}, total: ${ev.total}`);
progress = ev.loaded * 100 / ev.total;
}
複製代碼
onprogress
事件的event
對象中包含前端已經傳輸的數據信息ev.loaded
以及文件的總尺寸信息ev.total
,利用這些信息就能夠在頁面中顯示文件上傳進度
AJAX自身提供了取消操做,經過利用xhr.abort()
方法來取消掉整個xhr
的請求,固然若是僅僅想取消文件上傳而不是取消整個AJAX過程,也可使用xhr.upload.abort()
單獨的取消掉AJAX過程當中的文件上傳
<input type="file">
的onchange
事件在選擇文件發生變動的時候會觸發,利用事件中的event
對象的event.target.files
,能夠獲取到當前選擇的文件集合,遍歷該集合,根據file.type
來判斷文件類型,並利用window.URL.createObjectURL(file)
能夠拿到轉換事後的base64圖片地址,最後再給圖片img.src
設置路徑從而實現選擇回顯(圖片可使用createElement('img')
並body.appendChild()
,也可使用new Image()
和canvas
的dragImage()
方法來實現繪製)
/**
* 驗證圖片類型
* @param {*} type 文件類型
*/
function validateImage(type) {
return ['image/jpeg', 'image/png', 'image/jpg'].includes(type);
}
if (validateImage(file.type)) {
let image = document.createElement('img');
// URL.createObjectURL能夠接受File, Blob, MediaSource對象
image.style.height = '100px';
image.style.width = '100px';
image.src = window.URL.createObjectURL(file);
document.body.appendChild(image);
}
複製代碼
PS:因爲圖片加載對瀏覽器來講是異步的過程,若是要對圖片進行相關操做,請在img.onload
操做之後執行
在瞭解AJAX上傳的基礎上,其實拖拽上傳只須要知道如何獲取到拖拽文件對象,就可使用相同的方法進行上傳了。 拖拽也是有一系列事件,具體拖拽相關事件,能夠參見接下來的分享或者MDN Drag and Drop API
文件拖拽上傳的關鍵在於,能夠經過event.dataTransfer
獲取到拖拽信息。該對象存在的兩個對象屬性files
和items
,若是拖拽的內容是文件,那麼能夠遍歷files
對象,就能夠得到文件信息
html:
<div>
<p>拖拽上傳</p>
<div id="fileArea" class="file_area">拖拽到此區域上傳</div>
</div>
js:
let fileArea = document.querySelector('#fileArea')
fileArea.addEventListener('drop', ev => {
let files = ev.dataTransfer.files
for (let i = 0; i < files.length; i++) {
// 調用ajax相關內容
sendFile(files[i]);
}
// 防止瀏覽器直接打開文件
ev.preventDefault();
})
複製代碼
忽然某一天出現了目錄拖拽的需求,覺得和文件上傳是一樣能夠經過files
來獲取,結果發現不行。這個時候須要使用另外一個屬性對象items
,並利用File and Directory Entries API來處理items
。
首先利用item.webkitGetAsEntry()/item.getEntry()
獲取到FileEntry
,以後使用entry.createReader()
獲取到reader
對象,以後reader.readEntries
讀取信息並遞歸分別處理文件和文件夾,若是是文件經過entry.file()
的方式獲取文件信息
js:
fileArea.addEventListener('drop', ev => {
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
// 獲取entry對象
let entry = ev.dataTransfer.items[i].webkitGetAsEntry()
if (entry) {
scanFiles(entry, sendFile)
}
}
// 防止瀏覽器直接打開文件
ev.preventDefault();
})
function scanFiles (entry, callback) { // 瀏覽文件結構
// 若是是文件目錄,那麼繼續循環獲取到目錄下的文件
if (entry.isDirectory) {
let directoryReader = entry.createReader();
directoryReader.readEntries(entries => {
entries.forEach(entry => {
scanFiles(entry, callback);
})
}, err => {
console.log(err, err.message);
})
}
// 若是是文件,安麼添加到最後的文件數據集中
if (entry.isFile) {
i++
entry.file(file => {
callback(file, i);
}, err => {
console.log(err, err.message);
})
}
}
複製代碼
PS:
- 這裏尤爲要注意
entry.file()
方法,想要獲取到文件信息只能在回調函數中獲取- 因爲瀏覽器安全性問題,本地是不能直接訪問文件系統的,因此,若是以上的例子不在服務端運行,會報錯
DOMException
(這個問題花費了我N個小時),能夠全局安裝一個http-server來運行上面的代碼
編程真的是一件很好玩的事情,最近看算法的基礎,以爲真的頗有意思,前端編程也同樣,若是僅僅停留在使用組件上,真的很沒意思,有時間能夠多多看看各類原生的事件和方法,深刻研究一下框架至關有意思。超級感謝MDN啊,基本上能夠獲取到全部想要的信息
完整DEMO的:github.com/PatrickLh/f…