直接用 dragstart 写拖放易出错,因需手动处理 draggable 属性、dataTransfer.setData、dragover 阻止及 MIME 类型匹配;自定义元素可封装生命周期、自动处理事件与数据解析,提升复用性与健壮性。
原生拖放 API 表面简单,实际埋了几个坑:拖拽源必须设置 draggable="true",dragstart 里不调用 event.dataTransfer.setData() 就没数据、目标元素默认不响应 drop(因为 dragover 被浏览器阻止)、dataTransfer 的 MIME 类型写错就拿不到值。更麻烦的是,这些逻辑一旦分散在多个组件里,复用和测试都难。
用自定义元素把拖放生命周期收束到一个类里,关键不是“看起来高级”,而是把重复的 preventDefault、setData、事件监听打包。比如定义 <draggable-item> 和 <drop-zone> 两个元素:
draggable-item 在 connectedCallback 里自动加 draggable="true",并监听 dragstart,只暴露 getData 方法让你返回要传的数据(不用管 dataTransfer)drop-zone 自动处理 dragover 和 drop,触发 drop 自定义事件,detail 里带 dataTransfer 解析后的值(自动尝试 text/plain 和 application/json)data-type 属性声明数据类型,避免手动写 MIME 字符串示例:
<draggable-item data-type="task" getData='() => ({ id: 123, title: "Fix bug" })'>拖我</draggable-item><drop-zone ondrop="console.log(event.detail)">扔这儿</drop-zone>
即使封装了,还是常遇到 getData() 返回空字符串——这不是你代码写错了,而是浏览器策略卡的:
dragstart 会被忽略dataTransfer.setData("text/plain", ...) 和 getData("text/plain") 的 MIME 类型必须完全一致(大小写敏感,Text/Plain ≠ text/plain)application/json,值必须是字符串(JSON.stringify(obj)),不能直接传对象原生 API 对跨域限制很死:iframe 内容拖进主页面时,dataTransfer.items 是空的,且无法读取 files。这时候别硬扛:
dataTransfer,改用 postMessage 协作:拖拽开始时 iframe 向 parent 发消息,parent 记录拖拽状态;drop 时再由 parent 主动向 iframe 请求数据allow-iframe 属性,内部自动绑定 message 事件,但必须双方约定好消息格式(比如 { type: "drag-start", payload: { id: "file-789" } })drop 事件里同步读取 iframe 内容——它根本不会给你数据真正麻烦的从来不是怎么写 drag/drop,而是怎么让不同上下文之间信任地交换信息。封装只是省掉样板代码,边界问题还得靠设计兜住。