項目中有一個需求是希望富文本框可以直接粘貼圖片,不通過如下圖點擊文件上傳->選擇文件的方式。
解決思路
本着不自己造輪子的想法,遂查閱kindeditor
的官方文檔,發現並沒有輪子可以直接用,不過從編輯器初始化參數中看到有afterCreate
鉤子,同時編輯器(Editor) API中提供了insertHtml
接口。所以一條明確的造輪子思路就有了:
- 在
afterCreate
鉤子中給編輯器添加paste
事件監聽,在組件beforeDestoryed
鉤子函數中取消paste
事件監聽 - 當鼠標右鍵或者
Ctrl+V
粘貼時,觸發paste
事件,通過clipboardData
獲取粘貼板中的數據 - 使用
HTML5
中的FormData
對象創建表單對象,使用ajax
上傳圖片至服務端,獲得圖片URL
- 使用
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訪問粘貼板中本地路徑下的資源,所以複製本地路徑文件然後粘貼至富文本框中沒有反應。但是在實踐中發現拖拽的文件是可以訪問的,所以曲線救國,第二版增加拖拽上傳功能。與粘貼圖片步驟有兩點不同:
- 在
afterCreate
鉤子中給編輯器添加drop
監聽,在組件beforeDestoryed
鉤子函數中取消drop
事件監聽 - 在
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
主要有兩種方法:
- 使用
FileReader
,讀取本地File
數據然後轉換格式
function getBase64 (image, callback) {
const reader = new FileReader()
reader.addEventListener('load', () => {
if (typeof callback === 'function') {
callback(reader.result)
}
})
reader.readAsDataURL(image)
}
- 使用
canvas.toDataURL()
方法
數據源必須是CSSImageValue
、HTMLImageElement
、SVGImageElement
、HTMLVideoElement
、HTMLCanvasElement
、ImageBitmap
或者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
試試吧,哦豁~不得行。
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) |
將制定格式的數據賦值給剪貼板對象 | sFormat:Text 獲取字符串格式的數據;URL 獲取URL格式的數據 sData 字符串 |
必須 |
最終使用clipboardData
中的Files
獲取二進制文件流。
drop
事件中無法阻止IE
打開文件
若想阻止IE
默認打開拖拽的文件,必須阻止dragenter
和dragover
的默認行爲,或者說重寫dragenter
、dragover
和drop
事件
最終版輪子
// 上傳圖片
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
轉成二進制文件流。這個有時間在弄弄,功能實現纔是第一位,優化慢慢來~~~