Javascript-多文件拖动上传

写在开头

哈喽,各位好呀!😀

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

同时也是一个出游的好时机,小编已经开始在做旅游的打算了,打算到处去走走,亲近一下自然,调节一下最近苦闷的内心,这B班一刻也上不下去了😣。

回归正文,本章要分享的内容如下,请按需食需😉:

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

拖动事件

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

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

这里我们就不去细讲每个事件了,你可以自行去MDN上查阅😉。传送门

本次我们仅会用到如下四个事件:

  • dragenter:在可拖动的元素或者被选择的文本进入一个有效的放置目标时触发。
  • dragleave:在拖动的元素或选中的文本离开一个有效的放置目标时被触发。
  • dragover:在可拖动的元素或者被选择的文本被拖进一个有效的放置目标时(每几百毫秒)触发。
  • drop:在元素或文本选择被放置到有效的放置目标上时触发。为确保 drop 事件始终按预期触发,应当在处理 dragover 事件的代码部分始终包含 preventDefault() 调用。

可以稍微LookLook。👀

另外注意,为了创建自定义文件拖动的交互,我们需要在每个拖动事件中调用 event.preventDefault(),也就是阻止默认事件,否则当我们拖拽文件放置的时候会是浏览器来打开我们的文件,而不是由拖动事件来处理了。

这里我们可以进行一个统一处理:

javascript 复制代码
;['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();
}

布局样式

大概了解下拖动事件后,我们来开始进行布局与样式,随便简简单单搞一下就可以啦,不是重点。😗

直接贴代码:

html 复制代码
<!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 的命名,就没啦。

拖动功能

接下来,我们进入核心部分 - 拖动。

首先,第一件事,先获取我们的拖动放置区:

javascript 复制代码
const dropArea = document.getElementById('drop-area');

其次,我们先给放置区的边框添加一点拖动时的交互效果,提高用户体验👻。

javascript 复制代码
;['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

javascript 复制代码
dropArea.addEventListener('drop', dragEvent => {
  // 获取文件列表
  let files = e.dataTransfer.files;
  handleFiles(files);
}, false)

主要是从中获取拖动的文件对象列表

注意,如果你直接去打印 dragEvent 对象,展开后,发现 dataTransfer.files 为空的话。

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

有了文件对象后,这个功能我们就完成一大半了😯。

不过,要注意,上面拿到的文件对象列表 files 不是数组,它是一个伪数组。当我们实现 handleFiles 时,需要特别处理一下。

javascript 复制代码
function handleFiles(files) {
  // 转换文件对象列表的伪数组
  files = [...files];
  // 将文件对象上传到服务器
  files.forEach(uploadFile);
}

由于可能有多个文件对象一起上传,这里我们用了 .forEach 来循环迭代。

拿到正确的文件对象后,上传到服务器端就完事了👌。

javascript 复制代码
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 进行替换,但是由于我们可以进行多文件上传,所以还是用异步的叭😗。

具体过程如下:

javascript 复制代码
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 统一回显。

javascript 复制代码
function handleFiles(files) {
  files = [...files];
  files.forEach(uploadFile);
  // 回显文件
  files.forEach(previewFile);
}

上传进度

最后一个增幅功能,文件上传进度。😇

如果只是每次一个一个文件上传,那很简单,我们直接监听一下进度事件 progress 就可以完成。

但是,如果是多文件一起上传,Em......就要稍微费点劲了。😐

由于我们需要要考虑多文件上传的情况,所以我们需要来跟踪记录两个关键信息:总共要上传的文件数量(filesTotal)和已经成功上传的文数量(filesDoneTotal)。有了这两个数据,我们就能轻松计算出上传的进度了。

大概代码的呈现形式如下:

javascript 复制代码
// 初始化进度
function initializeProgress(numfiles) {
  // 重置进度条
  progressBar.value = 0;
  // 重置已上传数量
  filesDoneTotal = 0;
  // 文件总数量
  filesTotal = numfiles;
}

// 上传完成
function progressDone() {
  filesDoneTotal++;
  // 计算上传进度
  progressBar.value = filesDoneTotal / filesTotal * 100;
}

而具体在我们示例中的表现:

javascript 复制代码
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 两个方法就是上面先讲的两个方法放到实际业务中的变化而已。

实际使用:

javascript 复制代码
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 的相关参数信息,可以再细致瞧瞧。传送门

完整源码

最后,贴贴完整代码过程,你可以直接复制去玩玩看。

html 复制代码
<!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 延时先顶替着用用吧。😗


至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
恋猫de小郭11 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅18 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606118 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了18 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅19 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅19 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅19 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment19 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅20 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊20 小时前
jwt介绍
前端