JavaScript实现多文件拖动上传功能

 更新时间:2024年04月23日 10:17:46   作者:橙某人  
这篇文章主要为大家详细介绍了如何使用JavaScript实现多文件拖动上传功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

写在开头

哈喽,各位好呀!

近来开始回暖,风和日丽,晴空万里,连续几天都是好天气,好心情,真是一个很棒的季节呢。

本章要分享的内容如下,请按需食需:

大家对于文件上传功能肯定不陌生了,通常我们会直接采用UI框架提供的现成上传组件,因为从头开始编写一个上传组件确实较为繁琐。然而,这次小编将仅使用纯 JS 来实现一个拖动上传的功能。

拖动事件

而要完成拖动上传功能,首先,我们要来谈论的第一件事情就是其中的拖动事件。

浏览器总共有七个拖动相关的事件:dragdragenddragenterdragleavedragoverdragstartdrop

这里我们就不去细讲每个事件了,你可以自行去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;
}

应该比较好理解吧?

initializeProgressupdateProgress 两个方法就是上面先讲的两个方法放到实际业务中的变化而已。

实际使用:

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多文件拖动上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 如何防止JavaScript自动插入分号

    如何防止JavaScript自动插入分号

    JavaScript语言有一个机制:在解析时,能够在一句话后面自动插入一个分号,用来修改语句末尾遗漏的分号分隔符。然而,由于这个自动插入的分号与JavaScript语言的另一个机制发生了冲突,即所有空格符都被忽略,因此程序可以利用空格格式化代码
    2015-11-11
  • Bootstrap Chart组件使用教程

    Bootstrap Chart组件使用教程

    图表组件Chart.js是Bootstrap比较好用的组件之一,与一款收费的组件highchart类似,效果上来看免费与收费的产品相差还是有一点点的,不过功能上差不多能满足我们项目的需要,本文给大家介绍Bootstrap Chart组件使用,需要的朋友参考下吧
    2016-04-04
  • javascript 使用正则test( )第一次是 true,第二次是false

    javascript 使用正则test( )第一次是 true,第二次是false

    这篇文章主要介绍了使用正则test( )第一次是 true,第二次是false的相关资料,需要的朋友可以参考下
    2017-02-02
  • canvas 弹幕效果(实例分享)

    canvas 弹幕效果(实例分享)

    本文主要分享了canvas实现弹幕效果的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • JS格式化数字金额用逗号隔开保留两位小数

    JS格式化数字金额用逗号隔开保留两位小数

    JS格式化数字金额只留两位小数。写了个格式化函数。可以控制小数位数,自动四舍五入,感兴趣的朋友可以了解下
    2013-10-10
  • JS 事件冒泡 示例代码

    JS 事件冒泡 示例代码

    JS 事件冒泡 示例代码,大家可以参考下。
    2009-07-07
  • javascript中BOM基础知识总结

    javascript中BOM基础知识总结

    本文主要对javascript中BOM基础知识进行总结。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • js中常用的Tab切换效果(推荐)

    js中常用的Tab切换效果(推荐)

    下面小编就为大家带来一篇js中常用的Tab切换效果(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,祝大家游戏愉快哦
    2016-08-08
  • bootstrap常用组件之头部导航实现代码

    bootstrap常用组件之头部导航实现代码

    这篇文章主要介绍了bootstrap常用组件之头部导航实现代码,然后对个别常用属性进行了解释,需要的的朋友参考下吧
    2017-04-04
  • JS多物体实现缓冲运动效果示例

    JS多物体实现缓冲运动效果示例

    这篇文章主要介绍了JS多物体实现缓冲运动效果的方法,涉及javascript基于时间函数进行动态运算实现页面元素动态操作的相关技巧,需要的朋友可以参考下
    2016-12-12

最新评论