Summernote富文本编辑器实战踩坑与优化技巧

码农爱红 交互 阅读 1,467
赞 17 收藏
二维码
手机扫码查看
反馈

又踩坑了,Summernote 里粘贴图片上传老是失败

上周在搞一个富文本编辑器的功能,用的是 Summernote。本来以为就是个常规需求:用户粘贴图片,自动上传到服务器,返回 URL 插入编辑器。结果一上手就翻车——粘贴图片死活不触发上传,要么直接变成 base64 嵌在 HTML 里,要么干脆啥也不干。

Summernote富文本编辑器实战踩坑与优化技巧

我一开始以为是配置没写对,翻了官方文档,看到有个 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% 完美。比如用户同时粘贴文字和图片,我的代码只处理了图片,文字部分会被丢掉。但业务场景里基本没人这么干,而且改起来成本高,我就先这样了——能用就行,别过度优化。

踩坑提醒:这三点一定注意

除了上面说的,还有几个小坑差点让我崩溃:

  1. 跨域问题:本地开发时,如果后端没配 CORS,fetch 会静默失败(控制台都不报错!)。后来加了 mode: 'cors' 和后端配合才解决。
  2. 文件类型校验:clipboardData 里的文件可能没有扩展名,但 file.type 一般是可靠的(如 image/png),所以用 startsWith('image/') 判断更安全。
  3. 异步插入顺序:如果一次粘贴多张图,上传是并发的,但插入顺序可能乱。我试过加队列,但发现 Summernote 内部会自动按调用顺序插入,实际没乱,所以省了。

另外,如果你用的是 React 或 Vue 封装的 Summernote 组件,记得确保 onPaste 回调是在原生 DOM 初始化之后绑定的,不然可能监听不到事件。我之前在一个 Vue 项目里就因为组件生命周期问题,paste 事件没生效,排查了半小时。

其实还可以更优雅?

后来我翻 Summernote 的源码,发现它内部有个 handleImageUpload 方法,但没暴露出来。理论上可以 monkey patch 它,让粘贴也走统一上传逻辑。不过风险大,版本一升级就可能崩,所以没敢用。

也有同学建议用第三方库 like clipboard-polyfill,但我觉得为了一个功能引入新依赖不值。前端已经够臃肿了,能原生解决就原生。

总的来说,Summernote 对粘贴图片的支持确实不够友好,文档也缺这块。但只要抓住 onPaste + preventDefault + 手动上传 这个链路,基本就能搞定。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如怎么处理混合内容(文字+图片)粘贴,或者怎么在上传前压缩图片,我都挺感兴趣的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论