前言

最常见的文件上传方式是:

<input type="file" />

这种方式够用,但交互比较单一,不支持把文件直接拖进页面,也不支持用 Ctrl + V 粘贴图片。

如果想让上传体验更顺手,通常会把这三种方式一起支持:

  1. 点击选择文件
  2. 拖拽文件上传
  3. 粘贴图片上传

核心思路

三种方式本质上都是拿到 File 对象,再交给统一的上传逻辑处理。

  • input 通过 event.target.files 取文件
  • 拖拽通过 event.dataTransfer.files 取文件
  • 粘贴通过 event.clipboardData.items 取文件

只要最后都能得到 File,后面的预览、校验、上传都可以共用一套代码。

一个统一示例

<input id="fileInput" type="file" accept="image/*" />
<div id="dropZone" tabindex="0">拖拽文件到这里,或聚焦后直接粘贴图片</div>
<img id="preview" alt="预览图" />
const fileInput = document.getElementById('fileInput')
const dropZone = document.getElementById('dropZone')
const preview = document.getElementById('preview')

function handleFile(file) {
  if (!file) return

  const reader = new FileReader()
  reader.onload = event => {
    preview.src = event.target?.result ?? ''
  }
  reader.readAsDataURL(file)
}

fileInput.addEventListener('change', event => {
  const file = event.target.files?.[0]
  handleFile(file)
})

dropZone.addEventListener('dragover', event => {
  event.preventDefault()
})

dropZone.addEventListener('drop', event => {
  event.preventDefault()
  const file = event.dataTransfer?.files?.[0]
  handleFile(file)
})

dropZone.addEventListener('paste', event => {
  const items = event.clipboardData?.items ?? []

  for (const item of items) {
    if (item.kind === 'file') {
      const file = item.getAsFile()
      handleFile(file)
      break
    }
  }
})

需要注意的点

拖拽必须阻止默认行为

如果不在 dragoverdrop 里调用 event.preventDefault(),浏览器通常会直接尝试打开文件,而不是把它交给你的页面处理。

粘贴上传更适合图片

paste 最常见的是处理剪贴板里的图片,例如截图、微信截图、QQ 截图或者系统截图工具复制出来的内容。

文本当然也能粘贴,但文件上传场景里更常用的是图片。

粘贴区域最好可聚焦

div 这类元素默认不能获得焦点,所以一般要加上 tabindex="0",不然用户点过去后直接按 Ctrl + V,事件可能不会落在目标元素上。

适合哪些场景

  • 富文本编辑器上传图片
  • IM 聊天窗口发图
  • 工单系统上传截图
  • 管理后台快速上传附件

总结

上传文件不一定只能靠 input[type=file]

如果你想把交互做得更顺手,可以把这三种入口统一起来:

  1. change 处理点击选择
  2. drop 处理拖拽上传
  3. paste 处理粘贴上传

最后统一交给同一个 handleFile(),代码会更简单,体验也会更好。