网页传文件拖拽上传功能介绍
做上传功能的时候,最开始就是个 <input type="file">,用户点一下、在系统弹窗里翻半天找到文件、双击。文件藏在很深的目录时这套操作挺烦的。
拖拽明显顺手——直接把文件从桌面拖到页面上松手就完事。这篇讲怎么做。

一、区域和事件
先有个放文件的区域:
<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。做完这些,一个能拖、能选、有进度的上传区域就齐活了。
- 点赞
- 收藏
- 关注作者
评论(0)