1. 表單上傳
在AJAX還不流行的年代,表單上傳檔案是基本操作。表單上傳檔案很簡單,有兩個需要重點關注的屬性:
1.1 enctype
屬性用於設定form表單提交的時候資料編碼方式,一共有三種引數選擇:
application/x-www-form-urlencoded
傳送前編碼所有字元multipart/form-data
不對字元進行編碼text/plain
空格轉換為+
,但是不會對字元進行編碼
如果想要使用檔案上傳,必須指定為第二個屬性值:enctype=multipart/form-data
1.2 multiple
對於選擇檔案的時候如果想對檔案進行多選,那麼必須要設定<input type="file" multiple="multiple">
一個比較完整程式碼片段
<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>
複製程式碼
2. AJAX上傳
如果要實現頁面不重新整理的檔案上傳,有兩種常用的方案:
<iframe>
表單提交方案- AJAX方案
第一種方案在頁面中巢狀一個<iframe>
,將表單放置於<iframe>
中,此時完成表單提交不會發生全域性頁面重新整理。但是這個方案,隨著AJAX的逐漸完善以及前後端分離和單頁面應用的普及,輪為了很不常規的替代方案。
2.1 基本內容
實現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);
複製程式碼
如果表單中每個檔案想單獨傳送請求(傳送多次請求),可以獲取表單中檔案資訊並構建多個表單物件上傳
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
來支援檔案上傳操作
2.2 上傳進度
將上傳過程的上傳進度告訴使用者是一個很好的使用者互動行為,一方面避免使用者多次重複上傳,另一方面也是對使用者操作對反饋,告訴使用者系統正在處理他的操作。
監聽檔案上傳進度,個人認為要麼前端輪詢獲取後端的檔案寫入情況,要麼前端有支援上傳進度獲取對事件,其實確實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
,利用這些資訊就可以在頁面中顯示檔案上傳進度
2.3 取消上傳
AJAX自身提供了取消操作,通過利用xhr.abort()
方法來取消掉整個xhr
的請求,當然如果僅僅想取消檔案上傳而不是取消整個AJAX過程,也可以使用xhr.upload.abort()
單獨的取消掉AJAX過程中的檔案上傳
2.4 選擇圖片並上傳預覽
<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
操作以後執行
3. 拖拽上傳
在瞭解AJAX上傳的基礎上,其實拖拽上傳只需要知道如何獲取到拖拽檔案物件,就可以使用相同的方法進行上傳了。 拖拽也是有一系列事件,具體拖拽相關事件,可以參見接下來的分享或者MDN Drag and Drop API
3.1 檔案拖拽
檔案拖拽上傳的關鍵在於,可以通過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();
})
複製程式碼
3.2 目錄拖拽
突然某一天出現了目錄拖拽的需求,以為和檔案上傳是同樣可以通過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來執行上面的程式碼
4. 總結
程式設計真的是一件很好玩的事情,最近看演算法的基礎,覺得真的很有意思,前端程式設計也一樣,如果僅僅停留在使用元件上,真的很沒意思,有時間可以多多看看各種原生的事件和方法,深入研究一下框架相當有意思。超級感謝MDN啊,基本上可以獲取到所有想要的資訊
完整DEMO的:github.com/PatrickLh/f…