网页传文件拖拽上传功能介绍

举报
柠檬-夏日清爽 发表于 2026/10/10 10:41:14 2026/10/10
【摘要】 点"选择文件"、在弹出的窗口里翻半天、选中、确定——这套流程能省掉。这篇做个拖拽上传区域:dragover 不阻止默认行为 drop 就永远不触发这个必踩的坑、怎么拿到拖进来的文件、用 FormData 发出去、以及 fetch 拿不到上传进度这个限制该怎么绕。

做上传功能的时候,最开始就是个 <input type="file">,用户点一下、在系统弹窗里翻半天找到文件、双击。文件藏在很深的目录时这套操作挺烦的。

拖拽明显顺手——直接把文件从桌面拖到页面上松手就完事。这篇讲怎么做。
image.png

一、区域和事件

先有个放文件的区域:

<div id="dropZone" class="zone">
  把文件拖到这里,或者 <button id="pickBtn">点击选择</button>
  <input type="file" id="fileInput" multiple hidden>
</div>
.zone {
  border: 2px dashed #c3cbd6;
  border-radius: 8px;
  padding: 40px;
  text-align: center;
  color: #5b6b82;
}
.zone.over {
  border-color: #3a72d4;      /* 拖进来时高亮 */
  background: #eef4fd;
}

拖拽涉及四个事件,各自要不要阻止默认行为不一样:

事件 触发时机 要 preventDefault 吗
dragenter 拖着文件进入区域 要
dragover 在区域内移动(持续) 必须,不加 drop 不触发
dragleave 离开区域 不需要
drop 在区域内松手 要

代码这么写:

const zone = document.getElementById('dropZone')

zone.addEventListener('dragenter', e => {
  e.preventDefault()
  zone.classList.add('over')
})

zone.addEventListener('dragleave', e => {
  zone.classList.remove('over')
})

zone.addEventListener('drop', e => {
  e.preventDefault()
  zone.classList.remove('over')
  handleFiles(e.dataTransfer.files)
})

二、那个必踩的坑:dragover

光写上面三个事件是不够的,drop 根本不会触发。

原因是浏览器对拖拽有默认行为——拖文件到页面上,默认会用浏览器打开这个文件(比如拖张图片进去,页面直接跳转到那张图)。要让 drop 生效,必须在 dragover 里也阻止默认行为:

zone.addEventListener('dragover', e => {
  e.preventDefault()     // 这一行不加,drop 永远不会触发
})

这是拖拽上传最经典的坑,几乎每个人第一次做都会撞上。记住:dragover 和 drop 都要 preventDefault()。

顺带一提,拖进来的时候鼠标会显示"禁止"图标,想改成"复制"的话设置一下:

e.dataTransfer.dropEffect = 'copy'

三、拿到文件之后

e.dataTransfer.files 是一个 FileList,跟 <input type="file"> 的 files 是一样的东西,可以统一处理:

function handleFiles(fileList) {
  const files = Array.from(fileList)

  // 校验类型
  const allowed = ['image/jpeg', 'image/png', 'application/pdf']
  const bad = files.filter(f => !allowed.includes(f.type))
  if (bad.length) {
    alert(`不支持的文件类型:${bad.map(f => f.name).join('、')}`)
    return
  }

  // 校验大小,单文件 10MB
  const tooBig = files.filter(f => f.size > 10 * 1024 * 1024)
  if (tooBig.length) {
    alert('单个文件不能超过 10MB')
    return
  }

  upload(files)
}

Array.from() 转成数组才能用 filter、map 这些方法——FileList 是类数组但不是真数组。

四、发出去

用 FormData:

async function upload(files) {
  const fd = new FormData()
  files.forEach(f => fd.append('files', f))    // 同一个 key 追加多次

  const res = await fetch('/api/upload', {
    method: 'POST',
    body: fd,
    // 千万别手动设 Content-Type
  })
  const data = await res.json()
  console.log(data)
}

Content-Type 不要自己设。设了 multipart/form-data 反而会出错,因为浏览器需要自动加上 boundary 分隔符——那是 FormData 格式的一部分,你手写不出来。让浏览器自己去填。

五、进度条:fetch 做不到

想显示上传百分比,会发现 fetch 提供不了这个信息——fetch API 目前不支持上传进度,只能拿到下载进度。

要进度就得用老牌的 XMLHttpRequest:

function uploadWithProgress(files, onProgress) {
  return new Promise((resolve, reject) => {
    const fd = new FormData()
    files.forEach(f => fd.append('files', f))

    const xhr = new XMLHttpRequest()
    xhr.open('POST', '/api/upload')

    xhr.upload.onprogress = e => {
      if (e.lengthComputable) {
        const percent = Math.round((e.loaded / e.total) * 100)
        onProgress(percent)
      }
    }
    xhr.onload = () => resolve(JSON.parse(xhr.responseText))
    xhr.onerror = () => reject(new Error('上传失败'))

    xhr.send(fd)
  })
}

用法:

await uploadWithProgress(files, p => {
  progressBar.style.width = p + '%'
  progressText.textContent = p + '%'
})

注意是 xhr.upload.onprogress 不是 xhr.onprogress——后者是下载进度。

几个要注意的地方

  • 拖拽区域外的 drop 要拦掉。用户没拖到区域里、松手在页面别的地方,浏览器会直接打开那个文件,等于你的页面被顶掉了。在 document 上也拦一下:

    document.addEventListener('dragover', e => e.preventDefault())
    document.addEventListener('drop', e => e.preventDefault())
    
  • dragenter/dragleave 会疯狂触发。鼠标在区域内的子元素之间移动时,leave 和 enter 会反复冒泡,导致高亮闪烁。简单解法是给子元素加 pointer-events: none,或者用一个计数器判断。

  • 多选和目录。拖文件夹进来的话 files 里拿到的是目录项,type 是空字符串,大小也不对。要支持目录得用 dataTransfer.items 配合 webkitGetAsEntry(),复杂不少,一般场景直接过滤掉即可。

  • 重复上传同名文件。FormData 用同一个 key 追加多次,后端收到的就是一个数组,没问题。

  • 大文件别走这条路线。几十 MB 以上建议分片上传 + 断点续传,普通的 FormData 一次发上去,超时了就得重来。

  • 安全是后端的事。前端的类型和大小校验只是体验优化,用户绕过前端直接发请求照样能传。文件名、类型、大小、文件内容都得在后端再验一遍。

小结

拖拽上传的代码量不大,核心就四个事件加一个 FormData。真正卡人的就是那个 dragover 要 preventDefault()——不加的话 drop 永远不触发,而且报错信息完全没有,很难查。剩下的细节里,值得记住的是 fetch 拿不到上传进度、要进度就得用 XMLHttpRequest,以及别手动设 Content-Type。做完这些,一个能拖、能选、有进度的上传区域就齐活了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。