JavaScript實(shí)現(xiàn)多文件拖動(dòng)上傳功能
寫在開頭
哈嘍,各位好呀!
近來開始回暖,風(fēng)和日麗,晴空萬里,連續(xù)幾天都是好天氣,好心情,真是一個(gè)很棒的季節(jié)呢。
本章要分享的內(nèi)容如下,請按需食需:
大家對于文件上傳功能肯定不陌生了,通常我們會(huì)直接采用UI框架提供的現(xiàn)成上傳組件,因?yàn)閺念^開始編寫一個(gè)上傳組件確實(shí)較為繁瑣。然而,這次小編將僅使用純 JS
來實(shí)現(xiàn)一個(gè)拖動(dòng)上傳的功能。
拖動(dòng)事件
而要完成拖動(dòng)上傳功能,首先,我們要來談?wù)摰牡谝患虑榫褪瞧渲械耐蟿?dòng)事件。
瀏覽器總共有七個(gè)拖動(dòng)相關(guān)的事件:drag
、dragend
、dragenter
、dragleave
、dragover
、dragstart
、drop
。
這里我們就不去細(xì)講每個(gè)事件了,你可以自行去MDN上查閱。傳送門
本次我們僅會(huì)用到如下四個(gè)事件:
dragenter
:在可拖動(dòng)的元素或者被選擇的文本進(jìn)入一個(gè)有效的放置目標(biāo)時(shí)觸發(fā)。dragleave
:在拖動(dòng)的元素或選中的文本離開一個(gè)有效的放置目標(biāo)時(shí)被觸發(fā)。dragover
:在可拖動(dòng)的元素或者被選擇的文本被拖進(jìn)一個(gè)有效的放置目標(biāo)時(shí)(每幾百毫秒)觸發(fā)。drop
:在元素或文本選擇被放置到有效的放置目標(biāo)上時(shí)觸發(fā)。為確保drop
事件始終按預(yù)期觸發(fā),應(yīng)當(dāng)在處理dragover
事件的代碼部分始終包含preventDefault()
調(diào)用。
可以稍微LookLook。
另外注意,為了創(chuàng)建自定義文件拖動(dòng)的交互,我們需要在每個(gè)拖動(dòng)事件中調(diào)用 event.preventDefault()
,也就是阻止默認(rèn)事件,否則當(dāng)我們拖拽文件放置的時(shí)候會(huì)是瀏覽器來打開我們的文件,而不是由拖動(dòng)事件來處理了。
這里我們可以進(jìn)行一個(gè)統(tǒng)一處理:
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { // dropArea往下看 dropArea.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { // 阻止默認(rèn)事件 e.preventDefault(); // 阻止冒泡 e.stopPropagation(); }
布局樣式
大概了解下拖動(dòng)事件后,我們來開始進(jìn)行布局與樣式,隨便簡簡單單搞一下就可以啦,不是重點(diǎn)。
直接貼代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>拖動(dòng)上傳</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>將文件拖到此處或點(diǎn)擊上載</p> <input id="file" type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="file">點(diǎn)擊上載</label> <progress id="progress" max=100 value=0></progress> <div id="show-area"></div> </div> </body> </html>
結(jié)構(gòu)和樣式都比較簡單,就關(guān)鍵去注意 input
元素上加了一個(gè) onchange
事件與 multiple
允許多文件上傳,還有一些 id
的命名,就沒啦。
拖動(dòng)功能
接下來,我們進(jìn)入核心部分 - 拖動(dòng)。
首先,第一件事,先獲取我們的拖動(dòng)放置區(qū):
const dropArea = document.getElementById('drop-area');
其次,我們先給放置區(qū)的邊框添加一點(diǎn)拖動(dòng)時(shí)的交互效果,提高用戶體驗(yàn)。
;['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
來解決這個(gè)問題。
然后,我們來處理文件放置的事件 - drop
。
dropArea.addEventListener('drop', dragEvent => { // 獲取文件列表 let files = e.dataTransfer.files; handleFiles(files); }, false)
主要是從中獲取拖動(dòng)的文件對象列表。
注意,如果你直接去打印 dragEvent
對象,展開后,發(fā)現(xiàn) dataTransfer.files
為空的話。
你可以再打印 dragEvent.dataTransfer.files
瞧瞧。
有了文件對象后,這個(gè)功能我們就完成一大半了。
不過,要注意,上面拿到的文件對象列表 files
不是數(shù)組,它是一個(gè)偽數(shù)組。當(dāng)我們實(shí)現(xiàn) handleFiles
時(shí),需要特別處理一下。
function handleFiles(files) { // 轉(zhuǎn)換文件對象列表的偽數(shù)組 files = [...files]; // 將文件對象上傳到服務(wù)器 files.forEach(uploadFile); }
由于可能有多個(gè)文件對象一起上傳,這里我們用了 .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ù)覽
上面,我們完成文件拖動(dòng)上傳的基本功能,接下來我們來給它進(jìn)行"增幅",讓它變得更強(qiáng)。
既然是文件上傳,我們肯定是希望有回顯/預(yù)覽,這樣才能給用戶提供一個(gè)良好的體驗(yàn)。這里我們以回顯圖片為例,至于,其他文件類型.....Em...不好回顯。
回顯方式有幾種,最簡單的方式就是你可以等圖片上傳后,服務(wù)器給你返回URL,你直接顯示就行,但有時(shí)圖片很大的話,就意味你要等,或者需要占位符,這就很麻煩了。
而這次我們要探討的替換方案是從 drop
事件接收文件對象,再通過 FileReader API 進(jìn)行轉(zhuǎn)換、回顯。不過,這是一個(gè)異步的API,你也可以使用 FileReaderSync 進(jìn)行替換,但是由于我們可以進(jìn)行多文件上傳,所以還是用異步的叭。
具體過程如下:
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); } }
那在什么時(shí)候使用回顯呢?可以放在 uploadFile
回調(diào)方法中進(jìn)行一個(gè)一個(gè)回顯。也可以還是丟 handleFiles
方法中,用 .forEach
統(tǒng)一回顯。
function handleFiles(files) { files = [...files]; files.forEach(uploadFile); // 回顯文件 files.forEach(previewFile); }
上傳進(jìn)度
最后一個(gè)增幅功能,文件上傳進(jìn)度。
如果只是每次一個(gè)一個(gè)文件上傳,那很簡單,我們直接監(jiān)聽一下進(jìn)度事件 progress 就可以完成。
但是,如果是多文件一起上傳,Em......就要稍微費(fèi)點(diǎn)勁了。
由于我們需要要考慮多文件上傳的情況,所以我們需要來跟蹤記錄兩個(gè)關(guān)鍵信息:總共要上傳的文件數(shù)量(filesTotal
)和已經(jīng)成功上傳的文數(shù)量(filesDoneTotal
)。有了這兩個(gè)數(shù)據(jù),我們就能輕松計(jì)算出上傳的進(jìn)度了。
大概代碼的呈現(xiàn)形式如下:
// 初始化進(jìn)度 function initializeProgress(numfiles) { // 重置進(jìn)度條 progressBar.value = 0; // 重置已上傳數(shù)量 filesDoneTotal = 0; // 文件總數(shù)量 filesTotal = numfiles; } // 上傳完成 function progressDone() { filesDoneTotal++; // 計(jì)算上傳進(jìn)度 progressBar.value = filesDoneTotal / filesTotal * 100; }
而具體在我們示例中的表現(xiàn):
function handleFiles(files) { files = [...files]; // 初始化進(jìn)度 initializeProgress(files.length); files.forEach(uploadFile); files.forEach(previewFile); } let progressBar = document.getElementById('progress'); // 記錄文件的上傳進(jìn)度 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
兩個(gè)方法就是上面先講的兩個(gè)方法放到實(shí)際業(yè)務(wù)中的變化而已。
實(shí)際使用:
function uploadFile(file) { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); const url = '上傳地址'; xhr.open('POST', url, true); // 監(jiān)聽上傳進(jì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為每個(gè)文件序號,其實(shí)就是下標(biāo) updateProgress(i, 100); } else if (xhr.readyState == 4 && xhr.status != 200) { // 上傳失敗 } }) xhr.send(formData); }
關(guān)于進(jìn)度事件 progress
的相關(guān)參數(shù)信息,可以再細(xì)致瞧瞧。傳送門
完整源碼
最后,貼貼完整代碼過程,你可以直接復(fù)制去玩玩看。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>拖動(dòng)上傳</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>將文件拖到此處或點(diǎn)擊上載</p> <input id="file" type="file" multiple accept="image/*" onchange="handleFiles(this.files)"> <label class="button" for="file">點(diǎn)擊上載</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>
實(shí)際上傳部分,為了演示效果,小編使用 setTimeout
延時(shí)先頂替著用用吧。
到此這篇關(guān)于JavaScript實(shí)現(xiàn)多文件拖動(dòng)上傳功能的文章就介紹到這了,更多相關(guān)JavaScript多文件拖動(dòng)上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Javascript中substr和substring的區(qū)別
這篇文章主要介紹了Javascript中substr和substring的區(qū)別,非常的簡單明了,有需要的小伙伴可以來仔細(xì)看看。2015-09-09JS二級菜單不同實(shí)現(xiàn)方法分析【4種方法】
這篇文章主要介紹了JS二級菜單不同實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了4種不同的二級下拉菜單實(shí)現(xiàn)方法,需要的朋友可以參考下2018-12-1280行代碼寫一個(gè)Webpack插件并發(fā)布到npm
最近在學(xué)習(xí) Webpack 相關(guān)的原理,本文用80行代碼寫一個(gè)Webpack插件并發(fā)布到npm,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05Flutter實(shí)現(xiàn)仿微信底部菜單欄功能
這篇文章主要介紹了Flutter實(shí)現(xiàn)仿微信底部菜單欄,需要的朋友可以參考下2019-09-09JS判斷輸入的字符串是否是數(shù)字的方法(正則表達(dá)式)
下面小編就為大家?guī)硪黄狫S判斷輸入的字符串是否是數(shù)字的方法(正則表達(dá)式)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-11-11JS實(shí)現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動(dòng)閃爍提示效果代碼
這篇文章主要介紹了JS實(shí)現(xiàn)超簡潔網(wǎng)頁title標(biāo)題跑動(dòng)閃爍提示效果代碼,涉及JavaScript結(jié)合定時(shí)函數(shù)動(dòng)態(tài)操作頁面元素屬性的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10學(xué)習(xí)javascript面向?qū)ο?掌握創(chuàng)建對象的9種方式
這篇文章主要為大家介紹了創(chuàng)建對象的9種方式,幫助大家更好地學(xué)習(xí)javascript面向?qū)ο?,感興趣的小伙伴們可以參考一下2016-01-01基于JavaScript實(shí)現(xiàn)雪花許愿墻特效
新的一年就要到了,你一定有很多想許下的愿望吧!今天小編就為大家?guī)砹艘粋€(gè)基于Html+CSS+JavaScript實(shí)現(xiàn)的帶雪花的許愿墻特效,需要的可以了解一下2022-01-01