项目中有一个需求是希望富文本框可以直接粘贴图片,不通过如下图点击文件上传->选择文件的方式。
解决思路
本着不自己造轮子的想法,遂查阅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
转成二进制文件流。这个有时间在弄弄,功能实现才是第一位,优化慢慢来~~~
参考
- Javascript–clipboardData
- JS将图片转为base64编码
- kindeditor官网
- js,file或者blob图片文件转base64
- Kindeditor图片粘贴上传(chrome)
- MDN DragEvent
转载:https://blog.csdn.net/sinat_36521655/article/details/105393820