JavaScript實現(xiàn)多文件拖動上傳功能
寫在開頭
哈嘍,各位好呀!
近來開始回暖,風(fēng)和日麗,晴空萬里,連續(xù)幾天都是好天氣,好心情,真是一個很棒的季節(jié)呢。
本章要分享的內(nèi)容如下,請按需食需:

大家對于文件上傳功能肯定不陌生了,通常我們會直接采用UI框架提供的現(xiàn)成上傳組件,因為從頭開始編寫一個上傳組件確實較為繁瑣。然而,這次小編將僅使用純 JS 來實現(xiàn)一個拖動上傳的功能。
拖動事件
而要完成拖動上傳功能,首先,我們要來談?wù)摰牡谝患虑榫褪瞧渲械耐蟿邮录?/p>
瀏覽器總共有七個拖動相關(guān)的事件:drag、dragend、dragenter、dragleave、dragover、dragstart、drop。
這里我們就不去細講每個事件了,你可以自行去MDN上查閱。傳送門
本次我們僅會用到如下四個事件:
dragenter:在可拖動的元素或者被選擇的文本進入一個有效的放置目標(biāo)時觸發(fā)。dragleave:在拖動的元素或選中的文本離開一個有效的放置目標(biāo)時被觸發(fā)。dragover:在可拖動的元素或者被選擇的文本被拖進一個有效的放置目標(biāo)時(每幾百毫秒)觸發(fā)。drop:在元素或文本選擇被放置到有效的放置目標(biāo)上時觸發(fā)。為確保drop事件始終按預(yù)期觸發(fā),應(yīng)當(dāng)在處理dragover事件的代碼部分始終包含preventDefault()調(diào)用。
可以稍微LookLook。
另外注意,為了創(chuàng)建自定義文件拖動的交互,我們需要在每個拖動事件中調(diào)用 event.preventDefault(),也就是阻止默認事件,否則當(dāng)我們拖拽文件放置的時候會是瀏覽器來打開我們的文件,而不是由拖動事件來處理了。
這里我們可以進行一個統(tǒng)一處理:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
// dropArea往下看
dropArea.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
// 阻止默認事件
e.preventDefault();
// 阻止冒泡
e.stopPropagation();
}
布局樣式
大概了解下拖動事件后,我們來開始進行布局與樣式,隨便簡簡單單搞一下就可以啦,不是重點。
直接貼代碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>拖動上傳</title>
<style>
body {
padding: 0;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#drop-area {
border: 2px dashed #ccc;
border-radius: 10px;
width: 480px;
font-family: sans-serif;
margin: 100px auto;
padding: 20px;
}
#drop-area.highlight {
border-color: #409eff;
}
p {
margin-top: 0;
}
#file {
display: none;
}
.button {
display: block;
padding: 10px;
background: #409eff;
cursor: pointer;
border-radius: 5px;
margin-bottom: 10px;
color: #fff;
width: fit-content;
}
#show-area img {
width: 150px;
margin-top: 10px;
margin-right: 10px;
vertical-align: middle;
}
</style>
</head>
<body>
<div id="drop-area">
<p>將文件拖到此處或點擊上載</p>
<input id="file" type="file" multiple accept="image/*" onchange="handleFiles(this.files)">
<label class="button" for="file">點擊上載</label>
<progress id="progress" max=100 value=0></progress>
<div id="show-area"></div>
</div>
</body>
</html>
結(jié)構(gòu)和樣式都比較簡單,就關(guān)鍵去注意 input 元素上加了一個 onchange 事件與 multiple 允許多文件上傳,還有一些 id 的命名,就沒啦。
拖動功能
接下來,我們進入核心部分 - 拖動。
首先,第一件事,先獲取我們的拖動放置區(qū):
const dropArea = document.getElementById('drop-area');
其次,我們先給放置區(qū)的邊框添加一點拖動時的交互效果,提高用戶體驗。
;['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
})
;['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
})
function highlight(e) {
dropArea.classList.add('highlight');
}
function unhighlight(e) {
dropArea.classList.remove('highlight');
}
可以通過簡單的添加與刪除 class 來解決這個問題。
然后,我們來處理文件放置的事件 - drop。
dropArea.addEventListener('drop', dragEvent => {
// 獲取文件列表
let files = e.dataTransfer.files;
handleFiles(files);
}, false)
主要是從中獲取拖動的文件對象列表。
注意,如果你直接去打印 dragEvent 對象,展開后,發(fā)現(xiàn) dataTransfer.files 為空的話。

你可以再打印 dragEvent.dataTransfer.files 瞧瞧。

有了文件對象后,這個功能我們就完成一大半了。
不過,要注意,上面拿到的文件對象列表 files 不是數(shù)組,它是一個偽數(shù)組。當(dāng)我們實現(xiàn) handleFiles 時,需要特別處理一下。
function handleFiles(files) {
// 轉(zhuǎn)換文件對象列表的偽數(shù)組
files = [...files];
// 將文件對象上傳到服務(wù)器
files.forEach(uploadFile);
}
由于可能有多個文件對象一起上傳,這里我們用了 .forEach 來循環(huán)迭代。
拿到正確的文件對象后,上傳到服務(wù)器端就完事了。
function uploadFile(file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
const url = '上傳地址';
xhr.open('POST', url, true);
xhr.addEventListener('readystatechange', function(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
// 上傳成功-結(jié)束
}
else if (xhr.readyState == 4 && xhr.status != 200) {
// 上傳失敗
}
})
xhr.send(formData);
}
文件預(yù)覽
上面,我們完成文件拖動上傳的基本功能,接下來我們來給它進行"增幅",讓它變得更強。
既然是文件上傳,我們肯定是希望有回顯/預(yù)覽,這樣才能給用戶提供一個良好的體驗。這里我們以回顯圖片為例,至于,其他文件類型.....Em...不好回顯。
回顯方式有幾種,最簡單的方式就是你可以等圖片上傳后,服務(wù)器給你返回URL,你直接顯示就行,但有時圖片很大的話,就意味你要等,或者需要占位符,這就很麻煩了。
而這次我們要探討的替換方案是從 drop 事件接收文件對象,再通過 FileReader API 進行轉(zhuǎn)換、回顯。不過,這是一個異步的API,你也可以使用 FileReaderSync 進行替換,但是由于我們可以進行多文件上傳,所以還是用異步的叭。
具體過程如下:
function previewFile(file) {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = function() {
let img = document.createElement('img');
img.src = reader.result;
document.getElementById('show-area').appendChild(img);
}
}
那在什么時候使用回顯呢?可以放在 uploadFile 回調(diào)方法中進行一個一個回顯。也可以還是丟 handleFiles 方法中,用 .forEach 統(tǒng)一回顯。
function handleFiles(files) {
files = [...files];
files.forEach(uploadFile);
// 回顯文件
files.forEach(previewFile);
}
上傳進度
最后一個增幅功能,文件上傳進度。
如果只是每次一個一個文件上傳,那很簡單,我們直接監(jiān)聽一下進度事件 progress 就可以完成。
但是,如果是多文件一起上傳,Em......就要稍微費點勁了。
由于我們需要要考慮多文件上傳的情況,所以我們需要來跟蹤記錄兩個關(guān)鍵信息:總共要上傳的文件數(shù)量(filesTotal)和已經(jīng)成功上傳的文數(shù)量(filesDoneTotal)。有了這兩個數(shù)據(jù),我們就能輕松計算出上傳的進度了。
大概代碼的呈現(xiàn)形式如下:
// 初始化進度
function initializeProgress(numfiles) {
// 重置進度條
progressBar.value = 0;
// 重置已上傳數(shù)量
filesDoneTotal = 0;
// 文件總數(shù)量
filesTotal = numfiles;
}
// 上傳完成
function progressDone() {
filesDoneTotal++;
// 計算上傳進度
progressBar.value = filesDoneTotal / filesTotal * 100;
}
而具體在我們示例中的表現(xiàn):
function handleFiles(files) {
files = [...files];
// 初始化進度
initializeProgress(files.length);
files.forEach(uploadFile);
files.forEach(previewFile);
}
let progressBar = document.getElementById('progress');
// 記錄文件的上傳進度
let uploadProgress = [];
function initializeProgress(numFiles) {
progressBar.value = 0;
uploadProgress = [];
for (let i = numFiles; i > 0; i--) {
uploadProgress.push(0);
}
}
function updateProgress(fileNumber, percent) {
uploadProgress[fileNumber] = percent;
let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length;
progressBar.value = total;
}
應(yīng)該比較好理解吧?
initializeProgress 與 updateProgress 兩個方法就是上面先講的兩個方法放到實際業(yè)務(wù)中的變化而已。
實際使用:
function uploadFile(file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
const url = '上傳地址';
xhr.open('POST', url, true);
// 監(jiān)聽上傳進度事件
xhr.upload.addEventListener("progress", function (e) {
// e.loaded為上傳的字節(jié)數(shù),e.total為總的文件字節(jié)數(shù)
updateProgress(i, (e.loaded * 100.0 / e.total) || 100);
});
xhr.addEventListener('readystatechange', function(e) {
if (xhr.readyState == 4 && xhr.status == 200) {
// 上傳完成,i為每個文件序號,其實就是下標(biāo)
updateProgress(i, 100);
}
else if (xhr.readyState == 4 && xhr.status != 200) {
// 上傳失敗
}
})
xhr.send(formData);
}
關(guān)于進度事件 progress 的相關(guān)參數(shù)信息,可以再細致瞧瞧。傳送門
完整源碼
最后,貼貼完整代碼過程,你可以直接復(fù)制去玩玩看。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>拖動上傳</title>
<style>
body {
padding: 0;
margin: 0;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#drop-area {
border: 2px dashed #ccc;
border-radius: 10px;
width: 480px;
font-family: sans-serif;
margin: 100px auto;
padding: 20px;
}
#drop-area.highlight {
border-color: #409eff;
}
p {
margin-top: 0;
}
#file {
display: none;
}
.button {
display: block;
padding: 10px;
background: #409eff;
cursor: pointer;
border-radius: 5px;
margin-bottom: 10px;
color: #fff;
width: fit-content;
}
#show-area img {
width: 150px;
margin-top: 10px;
margin-right: 10px;
vertical-align: middle;
}
</style>
</head>
<body>
<div id="drop-area">
<p>將文件拖到此處或點擊上載</p>
<input id="file" type="file" multiple accept="image/*" onchange="handleFiles(this.files)">
<label class="button" for="file">點擊上載</label>
<progress id="progress" max=100 value=0></progress>
<div id="show-area" />
</div>
<script>
const dropArea = document.getElementById('drop-area');
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false)
document.body.addEventListener(eventName, preventDefaults, false)
})
;['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false)
})
;['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false)
})
dropArea.addEventListener('drop', (e) => {
let files = e.dataTransfer.files
handleFiles(files);
}, false)
function preventDefaults(e) {
e.preventDefault()
e.stopPropagation()
}
function highlight(e) {
dropArea.classList.add('highlight')
}
function unhighlight(e) {
dropArea.classList.remove('highlight')
}
let uploadProgress = []
let progressBar = document.getElementById('progress')
function initializeProgress(numFiles) {
progressBar.value = 0
uploadProgress = []
for (let i = numFiles; i > 0; i--) {
uploadProgress.push(0)
}
}
function updateProgress(fileNumber, percent) {
uploadProgress[fileNumber] = percent
let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length
progressBar.value = total
}
function handleFiles(files) {
files = [...files]
initializeProgress(files.length)
files.forEach(uploadFile)
files.forEach(previewFile)
}
function previewFile(file) {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onloadend = function () {
let img = document.createElement('img')
img.src = reader.result
document.getElementById('show-area').appendChild(img)
}
}
function uploadFile(file, i) {
setTimeout(() => {
updateProgress(i, 20 || 100)
}, 500)
setTimeout(() => {
updateProgress(i, 50 || 100)
}, 800)
setTimeout(() => {
updateProgress(i, 80 || 100)
}, 1000)
setTimeout(() => {
updateProgress(i, 100)
}, 1500)
}
</script>
</body>
</html>
實際上傳部分,為了演示效果,小編使用 setTimeout 延時先頂替著用用吧。
到此這篇關(guān)于JavaScript實現(xiàn)多文件拖動上傳功能的文章就介紹到這了,更多相關(guān)JavaScript多文件拖動上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Javascript中substr和substring的區(qū)別
這篇文章主要介紹了Javascript中substr和substring的區(qū)別,非常的簡單明了,有需要的小伙伴可以來仔細看看。2015-09-09
JS判斷輸入的字符串是否是數(shù)字的方法(正則表達式)
下面小編就為大家?guī)硪黄狫S判斷輸入的字符串是否是數(shù)字的方法(正則表達式)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11
JS實現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動閃爍提示效果代碼
這篇文章主要介紹了JS實現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動閃爍提示效果代碼,涉及JavaScript結(jié)合定時函數(shù)動態(tài)操作頁面元素屬性的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10
學(xué)習(xí)javascript面向?qū)ο?掌握創(chuàng)建對象的9種方式
這篇文章主要為大家介紹了創(chuàng)建對象的9種方式,幫助大家更好地學(xué)習(xí)javascript面向?qū)ο?,感興趣的小伙伴們可以參考一下2016-01-01

