必须在 dragover 事件中调用 e.preventDefault() 才能触发 drop 事件,否则浏览器默认阻止投放且不报错;仅监听 drop 无效,因 dragover 未阻止会导致 drop 根本不触发。

拖拽区域必须阻止 dragover 默认行为
浏览器对 drop 事件有严格限制:只要没在 dragover 时调用 e.preventDefault(),drop 就根本不会触发——界面会直接“吞掉”拖入动作,用户毫无反馈。
常见错误是只监听 drop,却漏掉 dragover 的拦截。正确写法要成对处理:
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', e => {
e.preventDefault(); // 必须!否则 drop 不会触发
e.stopPropagation();
});
dropZone.addEventListener('drop', async e => {
e.preventDefault();
const items = Array.from(e.dataTransfer.items);
await handleDroppedItems(items);
});
-
dataTransfer.items是DataTransferItemList,不是FileList,不能直接遍历files - 每个
item要用item.webkitGetAsEntry()判断是否为目录,再递归读取 - Firefox 不支持
webkitGetAsEntry,Safari 支持有限——得提前检测并 fallback 到单层文件提示
webkitdirectory 只在点击触发时生效,拖拽不认这个属性
给 加了 webkitdirectory,用户点按钮能选整个文件夹;但拖拽进来时,这个属性完全不起作用。浏览器不会自动把拖进来的文件夹当目录处理,必须手动解析 DataTransferItem。
所以自定义组件里不能只靠属性同步,得在 drop 回调里主动判断:
async function handleDroppedItems(items) {
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
} else if (item.kind === 'directory') {
const entry = item.webkitGetAsEntry();
if (entry && entry.isDirectory) {
await scanDirectory(entry, files); // 递归读取
}
}
}
return files;
}
- 递归函数
scanDirectory需用createReader().readEntries(),且要处理异步循环(不能用for...in) - Chrome/Edge 支持完整目录树;Firefox 返回的
item.kind永远是'file',无法识别目录 - 不要试图在非 Chromium 浏览器里强行模拟目录上传——容易卡死或报错
UI 状态反馈必须区分“可拖入”和“正在拖入”
只靠 CSS :hover 做拖拽高亮是错的。用户把文件拖到页面上但还没悬停到区域时,dragenter 已触发;离开区域时,dragleave 又可能被子元素中断,导致状态残留。
更可靠的做法是用布尔标记 + 显式事件控制:
let isDraggingOver = false;
dropZone.addEventListener('dragenter', () => {
isDraggingOver = true;
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', e => {
if (!dropZone.contains(e.relatedTarget)) {
isDraggingOver = false;
dropZone.classList.remove('drag-over');
}
});
dropZone.addEventListener('drop', () => {
isDraggingOver = false;
dropZone.classList.remove('drag-over');
});
- 别依赖
e.target === dropZone来判断离开,要用contains(e.relatedTarget) - 拖拽过程中如果鼠标划过子元素(如文字、图标),
dragleave会误触发,必须过滤 - 移动端没有拖拽概念,该交互需明确降级为点击选择,并隐藏拖拽相关 UI
accept 属性不能替代 JS 文件类型校验
accept=".pdf,.jpg" 只影响系统文件选择器的过滤界面,用户仍可手动修改后缀绕过。更重要的是:拖拽进来的文件完全不受 accept 控制——浏览器根本不检查,JS 必须自己读 file.type 或用 file.name.match() 校验。
校验逻辑要放在真正处理文件前,比如在 handleDroppedItems 后、上传前:
function isValidFileType(file) {
const validTypes = ['image/jpeg', 'image/png', 'application/pdf'];
const validExtensions = ['.jpg', '.jpeg', '.png', '.pdf'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
return validTypes.includes(file.type) || validExtensions.includes(ext);
}
-
file.type在某些系统下可能为空(比如 Windows 上拖拽的 .txt),必须结合后缀 fallback - 文件夹上传时,内部每个文件都要单独校验,不能只校验入口项
- 校验失败要立刻给出明确提示(如 toast 或高亮错误文件),而不是静默丢弃
实际封装时最易忽略的是跨浏览器行为差异——Chromium 的 webkitGetAsEntry 和 Firefox 的彻底不支持,不是加个 try-catch 就能掩盖的。必须在 UI 层明确区分能力边界,比如用 if ('webkitGetAsEntry' in DataTransferItem.prototype) 控制是否显示“拖拽文件夹”提示,否则用户会在 Firefox 里反复拖拽却无响应,以为功能坏了。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











