兼容IE和Chrome的图片粘贴、拖拽上传功能的实现

1,813 阅读3分钟

项目中有一个需求是希望富文本框可以直接粘贴图片,不通过如下图点击文件上传->选择文件的方式。

解决思路

本着不自己造轮子的想法,遂查阅kindeditor官方文档,发现并没有轮子可以直接用,不过从编辑器初始化参数中看到有afterCreate钩子,同时编辑器(Editor) API中提供了insertHtml接口。所以一条明确的造轮子思路就有了:

  1. afterCreate钩子中给编辑器添加paste事件监听,在组件beforeDestoryed钩子函数中取消paste事件监听
  2. 当鼠标右键或者Ctrl+V粘贴时,触发paste事件,通过clipboardData获取粘贴板中的数据
  3. 使用HTML5中的FormData对象创建表单对象,使用ajax上传图片至服务端,获得图片URL
  4. 使用insertHtml在富文本框光标处插入img标签图片

复制粘贴功能

handleCreated () {
  this.editor.edit.doc.addEventListener('paste', this.handlePaste)
},
handleBeforeDestoryed () {
  this.editor.edit.doc.removeEventListener('paste', this.handlePaste)
},
// 粘贴事件函数, 包括右键粘贴和ctrl+v
handlePaste (event) {
  // Chrome通过事件的clipboardData对象的items获得复制的图片
  let ele = event.clipboardData.items || []
  for (let i = 0; i < ele.length; ++i) {
    //判断文件类型
    if ( ele[i].kind == 'file' && /^image\//.test(ele[i].type)) {
      this.getBase64(ele[i].getAsFile(), this.insertImage)
      //得到二进制数据,并上传
      // this.uploadImage(ele[i].getAsFile(), this.insertImage)
    }
  }
},
// 上传图片
uploadImage (imageFile, callback) {
  // 创建表单对象,建立name=value的表单数据
  let formData = new FormData()
  formData.append('file', imageFile)

  // axios上传
  this.$http({
    method: 'POST',
    url: '/project/uploadImage?dir=image',
    contentType: 'multipart/form-data',
    data: formData
  }).then(res => {
    if (res.data.code === global.SUCCESS) {
      // 本人项目接口调用成功返回数据是URL
      if (typeof callback === 'function') {
        callback(res.data.body)
      }
    }
  }).catch(_ => {
    console.log("error")
  })
},
// 插入图片
insertImage (src) {
  let imgTag = "<img src='"+src+"' border='0'/>"
  // kindeditor insertHtml接口在光标处插入数据
  this.editor.insertHtml(imgTag)
}

需要注意的地方是,本人项目对axios做过封装,实际参数根据读者自己情况修改,关键就是请求头Content-Type需要设置为multipart/form-data

拖拽粘贴功能

因为浏览器安全策略问题,禁止JS访问粘贴板中本地路径下的资源,所以复制本地路径文件然后粘贴至富文本框中没有反应。但是在实践中发现拖拽的文件是可以访问的,所以曲线救国,第二版增加拖拽上传功能。与粘贴图片步骤有两点不同:

  1. afterCreate钩子中给编辑器添加drop监听,在组件beforeDestoryed钩子函数中取消drop事件监听
  2. dragEvent对象中的数据结构如下图,拖拽文件数据在dataTransfer中,有两种方式获取这些文件,第一种是访问items获取dataTransferItem,使用getAsFile()方法拿到二进制文件;第二种是直接通过Files拿到二进制文件。本文采用第二种。
    在这里插入图片描述
handleCreated () {
  // ...
  this.editor.edit.doc.addEventListener('drop', this.handleDrop)
},
handleBeforeDestoryed () {
  // ...
  this.editor.edit.doc.removeEventListener('drop', this.handleDrop)
},
// drop事件函数
handleDrop (event) {
  // 阻止冒泡
  event.stopPropagation()
  // 阻止浏览器默认打开文件的操作
  event.preventDefault()
  let files = event.dataTransfer.files
  for (let i = 0; i < files.length; ++i) {
    if (/^image\//.test(files[i].type)) {
      this.uploadImage(files[i], this.insertImage)
    }
  }
}

性能优化

上面代码在Chrome中可以实现非本地路径资源的复制粘贴功能以及本地资源的拖拽粘贴功能。但是实际使用过程中会发现如果图片很大且网络带宽小,很久才会粘贴成功,用户体验非常不好。因此在上述步骤中增加一步,本地将图片转成base64展示,然后上传至服务端,由服务端将base64转回图片文件存储。

图片转base64主要有两种方法:

  1. 使用FileReader,读取本地File数据然后转换格式
function getBase64 (image, callback) {
  const reader = new FileReader()
  reader.addEventListener('load', () => {
    if (typeof callback === 'function') {
      callback(reader.result)
    }
  })
  reader.readAsDataURL(image)
}
  1. 使用canvas.toDataURL()方法

数据源必须是CSSImageValueHTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmap或者OffscreenCanvas

function getBase64 (image) {
  // 创建canvas元素,并设置其宽高和图片一样,即不压缩图片
  let canvas = document.createElement("canvas")
  canvas.width = image.width
  canvas.height = image.height
  let ctx = canvas.getContext("2d")
  // 在画布上绘制图片
  ctx.drawImage(image, 0, 0, image.width, image.height)
  // 使用toDataURL方法指定格式,获取Base64编码的URL
  let dataURL = canvas.toDataURL(image.type)
  // 释放,垃圾回收
  canvas = null
  return dataURL
}

无论从clipboardData还是dataTransfer中获取到的都是二进制文件流File,所以本文使用第一种方法。

IE的坑

一个满足基本使用的轮子造出来了,Chrome测试没啥问题,上IE试试吧,哦豁~不得行。

  1. clipboardData数据结构限制 在Chrome中,clipboardData如下,可以通过items获得粘贴板中的数据。
    在这里插入图片描述
    IE浏览器中如下,使用getData(format)方法获得数据。
    在这里插入图片描述
    毕竟clipboardData还不是W3C标准,每个浏览器实现不一样早想得到的,不仅如此,IE目前还只支持获取字符串格式URL格式的数据,这一点就很蛋疼。
方法 描述 参数 参数是否必须
clearData([sFormat]) 从剪贴板删除一种或多种数据格式 Text 移除字符串格式数据
URL 移除URL格式数据
File 移除File格式数据文件
HTML 移除HTML格式数据文件
Image 移除Image格式数据文件
可选
getData(sFormat) 从剪贴板上获取指定格式的数据 Text 获取字符串格式的数据
URL 获取URL格式的数据
必须
setData(sFormat,sData) 将制定格式的数据赋值给剪贴板对象 sFormatText 获取字符串格式的数据;URL 获取URL格式的数据
sData 字符串
必须

最终使用clipboardData中的Files获取二进制文件流。

  1. drop事件中无法阻止IE打开文件 若想阻止IE默认打开拖拽的文件,必须阻止dragenterdragover的默认行为,或者说重写dragenterdragoverdrop事件

最终版轮子

// 上传图片
uploadImage (imageFile, callback) {
  // 创建表单对象,建立name=value的表单数据
  let formData = new FormData()
  formData.append('file', imageFile)

  // axios上传
  this.$http({
    method: 'POST',
    url: '/project/uploadImage?dir=image',
    contentType: 'multipart/form-data',
    data: formData
  }).then(res => {
    if (res.data.code === global.SUCCESS) {
      // 本人项目接口调用成功返回数据是URL
      if (typeof callback === 'function') {
        callback(res.data.body)
      }
    }
  }).catch(_ => {
    console.log("error")
  })
},
// 插入图片
insertImage (src) {
  let imgTag = "<img src='"+src+"' border='0'/>"
  // kindeditor insertHtml接口在光标处插入数据
  this.editor.insertHtml(imgTag)
},
// 文件流转base64
getBase64 (image, callback) {
  const reader = new FileReader()
  reader.addEventListener('load', () => {
    if (typeof callback === 'function') {
      callback(reader.result)
    }
  })
  reader.readAsDataURL(image)
},
// 阻止冒泡和默认事件
preventEvent (event) {
  event.stopPropagation()
  event.preventDefault()
},
// kindeditor afterCreate回调函数
handleCreated () {
  let doc = this.editor.edit.doc
  doc.addEventListener('paste', this.handlePaste)
  doc.addEventListener('drop', this.handleDrop)
  doc.addEventListener("dragenter", this.preventEvent)
  doc.addEventListener("dragover", this.preventEvent)
},
// beforeDestoryed钩子
handleBeforeDestoryed () {
  let doc = this.editor.edit.doc
  doc.removeEventListener('paste', this.handlePaste)
  doc.removeEventListener('drop', this.handleDrop)
  doc.removeEventListener('dragenter', this.preventEvent)
  doc.removeEventListener('dragover', this.preventEvent)
},
// paste事件函数, 包括右键粘贴和ctrl+v
handlePaste (event) {
  // IE粘贴板数据clipboardData在全局对象中,通过clipboardData对象的files获得复制的图片
  let files = (window.clipboardData || event.clipboardData).files || []
  for (let i = 0; i < files.length; ++i) {
    //判断文件类型
    if (/^image\//.test(files[i].type)) {
      //得到二进制数据,并上传
      this.uploadImage(files[i], this.insertImage)
    }
  }
},
// drop事件函数
handleDrop (event) {
  this.preventEvent(event)

  let files = event.dataTransfer.files
  for (let i = 0; i < files.length; ++i) {
    if (/^image\//.test(files[i].type)) {
      this.getBase64(files[i], this.insertImage)
      // this.uploadImage(files[i], this.insertImage)
    }
  }
}

还可以优化的地方

目前是直接使用原图,正常情况下应该将原图压缩,减少数据库和带宽成本。一般做法是使用Image对象生成HTMLImageElement,设置src后再通过canvas压缩,重新获取base64,在将base64转成二进制文件流。这个有时间在弄弄,功能实现才是第一位,优化慢慢来~~~

参考

  1. Javascript--clipboardData
  2. JS将图片转为base64编码
  3. kindeditor官网
  4. js,file或者blob图片文件转base64
  5. Kindeditor图片粘贴上传(chrome)
  6. MDN DragEvent