文件上傳是一個很基礎的內容,有很多的應用場景,但是前端各種庫和框架實在是太便利了,根本不用了解到用原生的是怎么實現的,一遇到問題就各種懵逼,最近剛好經歷了幾種文件上傳的需求,就以此來作為開年的第一篇分享
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的:https://github.com/PatrickLh/file-upload