Summernote富文本编辑器实战踩坑与优化技巧
又踩坑了,Summernote 里粘贴图片上传老是失败
上周在搞一个富文本编辑器的功能,用的是 Summernote。本来以为就是个常规需求:用户粘贴图片,自动上传到服务器,返回 URL 插入编辑器。结果一上手就翻车——粘贴图片死活不触发上传,要么直接变成 base64 嵌在 HTML 里,要么干脆啥也不干。
我一开始以为是配置没写对,翻了官方文档,看到有个 onImageUpload 回调,赶紧照着抄:
$('#summernote').summernote({
callbacks: {
onImageUpload: function(files) {
console.log('上传图片', files);
// 上传逻辑
}
}
});
但问题来了:粘贴图片时,这个回调根本没触发!只有点工具栏的“插入图片”按钮才走这个流程。我折腾了半天,查 issue、看源码,才发现 Summernote 默认对粘贴内容做了处理,如果是图片,会直接转成 base64 插入,压根不走 onImageUpload。
这里我踩了个大坑:以为所有图片插入都会走同一个上传通道,结果粘贴和手动上传是两套逻辑。
三种方案对比,我选了最简单的
后来试了下发现,要处理粘贴图片,得监听 paste 事件,自己拦截剪贴板里的图片数据。网上搜了一圈,大概有三种思路:
- 方案一:完全禁用默认粘贴行为,自己解析 clipboardData,提取图片文件,手动上传后再插入
- 方案二:用 MutationObserver 监听 DOM 变化,一旦发现 base64 图片就替换成上传后的 URL(太 hack,性能差)
- 方案三:利用 Summernote 的
onPaste回调(v0.8.18+ 支持),在里面处理图片
我项目用的 Summernote 版本是 0.8.20,所以直接上了方案三。但文档里对 onPaste 的说明特别少,连参数都没写清楚,只能靠 console.log 摸索。
试了几次发现,onPaste 接收三个参数:e, ne, oe,其中 e 是原生 paste 事件对象,关键数据在 e.originalEvent.clipboardData 里。
核心代码就这几行
最后搞定的逻辑是这样的:在 onPaste 里检查剪贴板有没有图片,如果有,就阻止默认行为,然后手动读取文件、上传、插入 URL。代码如下:
$('#summernote').summernote({
height: 300,
callbacks: {
onPaste: function(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
// 检查是否有图片
const items = clipboardDdata.items || [];
let hasImage = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
hasImage = true;
break;
}
}
if (hasImage) {
e.preventDefault(); // 阻止默认粘贴(避免 base64)
// 从 clipboardData 获取文件
const files = Array.from(clipboardData.files).filter(file =>
file.type.startsWith('image/')
);
if (files.length > 0) {
uploadImagesAndInsert(files);
}
}
}
}
});
function uploadImagesAndInsert(files) {
const editor = $('#summernote');
files.forEach(file => {
const formData = new FormData();
formData.append('file', file);
fetch('https://jztheme.com/api/upload-image', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.url) {
// 注意:必须在 Summernote 的上下文中插入
editor.summernote('insertImage', data.url, file.name);
}
})
.catch(err => {
console.error('上传失败', err);
// 这里可以加个提示,比如用 toastr
});
});
}
这里注意几个细节:
e.preventDefault()必须加,否则 Summernote 还是会把 base64 插进去clipboardData.files在某些浏览器(比如旧版 Safari)可能为空,但主流 Chrome/Firefox 没问题editor.summernote('insertImage', url, filename)这个 API 会自动包裹成<img src="...">,不用自己拼 HTML
不过说实话,这个方案也不是 100% 完美。比如用户同时粘贴文字和图片,我的代码只处理了图片,文字部分会被丢掉。但业务场景里基本没人这么干,而且改起来成本高,我就先这样了——能用就行,别过度优化。
踩坑提醒:这三点一定注意
除了上面说的,还有几个小坑差点让我崩溃:
- 跨域问题:本地开发时,如果后端没配 CORS,fetch 会静默失败(控制台都不报错!)。后来加了
mode: 'cors'和后端配合才解决。 - 文件类型校验:clipboardData 里的文件可能没有扩展名,但
file.type一般是可靠的(如image/png),所以用startsWith('image/')判断更安全。 - 异步插入顺序:如果一次粘贴多张图,上传是并发的,但插入顺序可能乱。我试过加队列,但发现 Summernote 内部会自动按调用顺序插入,实际没乱,所以省了。
另外,如果你用的是 React 或 Vue 封装的 Summernote 组件,记得确保 onPaste 回调是在原生 DOM 初始化之后绑定的,不然可能监听不到事件。我之前在一个 Vue 项目里就因为组件生命周期问题,paste 事件没生效,排查了半小时。
其实还可以更优雅?
后来我翻 Summernote 的源码,发现它内部有个 handleImageUpload 方法,但没暴露出来。理论上可以 monkey patch 它,让粘贴也走统一上传逻辑。不过风险大,版本一升级就可能崩,所以没敢用。
也有同学建议用第三方库 like clipboard-polyfill,但我觉得为了一个功能引入新依赖不值。前端已经够臃肿了,能原生解决就原生。
总的来说,Summernote 对粘贴图片的支持确实不够友好,文档也缺这块。但只要抓住 onPaste + preventDefault + 手动上传 这个链路,基本就能搞定。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如怎么处理混合内容(文字+图片)粘贴,或者怎么在上传前压缩图片,我都挺感兴趣的。

暂无评论