JavaScript实现多文件拖动上传功能
作者:橙某人
写在开头
哈喽,各位好呀!
近来开始回暖,风和日丽,晴空万里,连续几天都是好天气,好心情,真是一个很棒的季节呢。
本章要分享的内容如下,请按需食需:
大家对于文件上传功能肯定不陌生了,通常我们会直接采用UI框架提供的现成上传组件,因为从头开始编写一个上传组件确实较为繁琐。然而,这次小编将仅使用纯 JS
来实现一个拖动上传的功能。
拖动事件
而要完成拖动上传功能,首先,我们要来谈论的第一件事情就是其中的拖动事件。
浏览器总共有七个拖动相关的事件:drag
、dragend
、dragenter
、dragleave
、dragover
、dragstart
、drop
。
这里我们就不去细讲每个事件了,你可以自行去MDN上查阅。传送门
本次我们仅会用到如下四个事件:
dragenter
:在可拖动的元素或者被选择的文本进入一个有效的放置目标时触发。dragleave
:在拖动的元素或选中的文本离开一个有效的放置目标时被触发。dragover
:在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。drop
:在元素或文本选择被放置到有效的放置目标上时触发。为确保drop
事件始终按预期触发,应当在处理dragover
事件的代码部分始终包含preventDefault()
调用。
可以稍微LookLook。
另外注意,为了创建自定义文件拖动的交互,我们需要在每个拖动事件中调用 event.preventDefault()
,也就是阻止默认事件,否则当我们拖拽文件放置的时候会是浏览器来打开我们的文件,而不是由拖动事件来处理了。
这里我们可以进行一个统一处理:
;['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>
结构和样式都比较简单,就关键去注意 input
元素上加了一个 onchange
事件与 multiple
允许多文件上传,还有一些 id
的命名,就没啦。
拖动功能
接下来,我们进入核心部分 - 拖动。
首先,第一件事,先获取我们的拖动放置区:
const dropArea = document.getElementById('drop-area');
其次,我们先给放置区的边框添加一点拖动时的交互效果,提高用户体验。
;['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
对象,展开后,发现 dataTransfer.files
为空的话。
你可以再打印 dragEvent.dataTransfer.files
瞧瞧。
有了文件对象后,这个功能我们就完成一大半了。
不过,要注意,上面拿到的文件对象列表 files
不是数组,它是一个伪数组。当我们实现 handleFiles
时,需要特别处理一下。
function handleFiles(files) { // 转换文件对象列表的伪数组 files = [...files]; // 将文件对象上传到服务器 files.forEach(uploadFile); }
由于可能有多个文件对象一起上传,这里我们用了 .forEach
来循环迭代。
拿到正确的文件对象后,上传到服务器端就完事了。
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) { // 上传成功-结束 } else if (xhr.readyState == 4 && xhr.status != 200) { // 上传失败 } }) xhr.send(formData); }
文件预览
上面,我们完成文件拖动上传的基本功能,接下来我们来给它进行"增幅",让它变得更强。
既然是文件上传,我们肯定是希望有回显/预览,这样才能给用户提供一个良好的体验。这里我们以回显图片为例,至于,其他文件类型.....Em...不好回显。
回显方式有几种,最简单的方式就是你可以等图片上传后,服务器给你返回URL,你直接显示就行,但有时图片很大的话,就意味你要等,或者需要占位符,这就很麻烦了。
而这次我们要探讨的替换方案是从 drop
事件接收文件对象,再通过 FileReader API 进行转换、回显。不过,这是一个异步的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
回调方法中进行一个一个回显。也可以还是丢 handleFiles
方法中,用 .forEach
统一回显。
function handleFiles(files) { files = [...files]; files.forEach(uploadFile); // 回显文件 files.forEach(previewFile); }
上传进度
最后一个增幅功能,文件上传进度。
如果只是每次一个一个文件上传,那很简单,我们直接监听一下进度事件 progress 就可以完成。
但是,如果是多文件一起上传,Em......就要稍微费点劲了。
由于我们需要要考虑多文件上传的情况,所以我们需要来跟踪记录两个关键信息:总共要上传的文件数量(filesTotal
)和已经成功上传的文数量(filesDoneTotal
)。有了这两个数据,我们就能轻松计算出上传的进度了。
大概代码的呈现形式如下:
// 初始化进度 function initializeProgress(numfiles) { // 重置进度条 progressBar.value = 0; // 重置已上传数量 filesDoneTotal = 0; // 文件总数量 filesTotal = numfiles; } // 上传完成 function progressDone() { filesDoneTotal++; // 计算上传进度 progressBar.value = filesDoneTotal / filesTotal * 100; }
而具体在我们示例中的表现:
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; }
应该比较好理解吧?
initializeProgress
与 updateProgress
两个方法就是上面先讲的两个方法放到实际业务中的变化而已。
实际使用:
function uploadFile(file) { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); const url = '上传地址'; xhr.open('POST', url, true); // 监听上传进度事件 xhr.upload.addEventListener("progress", function (e) { // e.loaded为上传的字节数,e.total为总的文件字节数 updateProgress(i, (e.loaded * 100.0 / e.total) || 100); }); xhr.addEventListener('readystatechange', function(e) { if (xhr.readyState == 4 && xhr.status == 200) { // 上传完成,i为每个文件序号,其实就是下标 updateProgress(i, 100); } else if (xhr.readyState == 4 && xhr.status != 200) { // 上传失败 } }) xhr.send(formData); }
关于进度事件 progress
的相关参数信息,可以再细致瞧瞧。传送门
完整源码
最后,贴贴完整代码过程,你可以直接复制去玩玩看。
<!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
延时先顶替着用用吧。
到此这篇关于JavaScript实现多文件拖动上传功能的文章就介绍到这了,更多相关JavaScript多文件拖动上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!