兼容IE和Chrome的富文本框圖片粘貼、拖拽上傳功能的實現

項目中有一個需求是希望富文本框可以直接粘貼圖片,不通過如下圖點擊文件上傳->選擇文件的方式。
1.png

解決思路

本着不自己造輪子的想法,遂查閱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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章