ueditor换wangeditor-next

🚧 Vue2 项目富文本替换实践:从 UEditor 到 WangEditor Next

📌 项目背景

项目原本使用的是 UEditor,但遇到一些不可忽视的问题:

  • ❌ 官方已停止维护,BUG 无人修复
  • ❌ 体积庞大,功能臃肿,加载缓慢
  • ❌ UI 过时,扩展性差

因此,我们决定迁移到更现代、更灵活的富文本编辑器 ------ WangEditor Next


💡 为什么选 WangEditor Next?

  • ✅ 支持 Vue3 版本,也兼容 Vue2(通过组件封装)
  • ✅ 中文社区活跃,文档完善,上手简单
  • ✅ 支持模块化扩展,便于定制图片、视频上传等逻辑

⚠️ 遇到的实际问题 & 解决方向

1️⃣ 自定义上传图片、视频

WangEditor 默认使用 base64 插图或请求默认接口,实际项目需要:

  • 上传图片到对象存储(如阿里 OSS、七牛云)
  • 成功后插入返回的 URL 地址

📍可通过配置 customUpload 钩子,实现完全自定义上传逻辑。

2️⃣ Word 粘贴带有本地图片(file://

很多用户直接从 Word 中复制内容粘贴进编辑器,此时会出现类似:

ini 复制代码
<img src="file://C:/Users/xxx/AppData/Local/...">

✔️ 解决思路:

  • 拦截粘贴内容
  • 查找 <img src^="file://"> 标签并移除或提示用户重新插入图片

3️⃣ 拖拽外网图片插入

用户可能直接拖一张网页图片进编辑器,此时图片地址可能是外链,比如:

ini 复制代码
<img src="https://xxx.com/image.jpg">

⚠️ 问题在于:

  • 外链图片容易失效或跨域加载失败
  • 项目需要统一上传图片到自有服务

✔️ 解决思路:

  • 拦截 drop 事件

package.json 文件

js 复制代码
    "@wangeditor-next/editor": "^5.6.34",
    "@wangeditor-next/editor-for-vue2": "^1.0.2",

Wandeditor 组件

js 复制代码
<template>
  <div style="border: 1px solid #ccc">
    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
    <Editor ref="editorRef" style="height: 500px; overflow-y: hidden" v-model="html" :defaultConfig="editorConfig" :mode="mode" @onCreated="onCreated" @onChange="onChange" />
    <base-button @click="getHtml">获取HTML</base-button>
  </div>
</template>
<script>
import { Editor, Toolbar } from '@wangeditor-next/editor-for-vue2'
// 上传文件Promise的方法, 成功后返回:文件名/系统文件地址/文件预览地址 {fileName,filePath,previewUrl}
import { uploadFile } from '@/components/common/common-upload/index.js'

export default {
  components: { Editor, Toolbar },
  data() {
    return {
      editor: null,
      html: '',
      // 禁用菜单功能
      toolbarConfig: { excludeKeys: ['fontFamily', 'insertImage', 'insertVideo'] },
      editorConfig: {
        placeholder: '请输入内容...',
        maxLength: 10000,
        pasteIgnoreImg: true,
        // 配置自定义上传图片和视频
        MENU_CONF: {
          uploadImage: {
            customUpload: this.customUploadImage
          },
          uploadVideo: {
            customUpload: this.customUploadVideo
          }
        },
        // 可根据需求,配置图片和视频的操作功能
        hoverbarKeys: {
          image: { menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'editorImageSizeMenu', 'deleteImage'] },
          video: { menuKeys: ['editVideoSize'] }
        }
      },
      mode: 'default' // or 'simple'
    }
  },
  created() {},
  methods: {
    onCreated(editor) {
      this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
    },
    // 自定义-上传图片
    customUploadImage(file, insertFn) {
      uploadFile(file)
        .then(fileData => {
          const { fileName, previewUrl } = fileData
          insertFn(previewUrl, fileName, previewUrl)
        })
        .catch(err => {
          this.$message({
            type: 'error',
            message: this.$t('Upload_failed')
          })
        })
    },
    // 自定义-上传视频
    customUploadVideo(file, insertFn) {
      uploadFile(file)
        .then(async fileData => {
          const { fileName, previewUrl } = fileData
          insertFn(previewUrl)
        })
        .catch(err => {
          this.$message({
            type: 'error',
            message: this.$t('Upload_failed')
          })
        })
    },
    // 处理从外部复制/粘贴的图片地址问题
    onChange(editor) {
      let images = editor.getElemsByType('image')
      let fileOsArray = []
      images.forEach(item => {
        // fileOsArray记录本地图片
        if (item.src.startsWith('file://')) {
          fileOsArray.push(item.src)
        }
      })
      if (!fileOsArray.length) return

      // 替换所有本地图片,提示错误图片信息
      let html = editor.getHtml()
      fileOsArray.forEach(fileOs => {
        // 使用正则替换所有相同的 file:// 路径(可能重复)
        const escaped = fileOs.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // 正则转义
        html = html.replace(
          new RegExp(escaped, 'g'),
          // 这里可以改成你需要替换的图片地址或者图片base64
          'data:image/png;base64,***'
        )
      })
      editor.setHtml(html)
    },
    getHtml() {
      const html = this.editor.getHtml()
      console.log(html)
    },
    handleDrop(e) {
      e.preventDefault()
      e.stopPropagation()
    }
  },
  mounted() {
    // 禁止拖拽事件
    const el = this.$refs.editorRef.$el
    if (el) {
      el.addEventListener('drop', this.handleDrop, true)
      el.addEventListener('dragover', this.handleDrop, true)
    }
    setTimeout(() => {
      this.html = '<p>模拟接口获取数据</p>'
    }, 1000)
  },
  beforeDestroy() {
    const el = this.$refs.editorRef.$el
    if (el) {
      el.removeEventListener('dragover', this.handleDrop, true)
      el.removeEventListener('drop', this.handleDrop, true)
    }
    const editor = this.editor
    if (editor == null) return
    editor.destroy() // 组件销毁时,及时销毁编辑器
  }
}
</script>
<style src="@wangeditor-next/editor/dist/css/style.css"></style>
相关推荐
Jonathan Star9 小时前
沉浸式雨天海岸:用A-Frame打造WebXR互动场景
前端·javascript
工业甲酰苯胺9 小时前
实现 json path 来评估函数式解析器的损耗
java·前端·json
老前端的功夫9 小时前
Web应用的永生之术:PWA落地与实践深度指南
java·开发语言·前端·javascript·css·node.js
LilySesy10 小时前
ABAP+WHERE字段长度不一致报错解决
java·前端·javascript·bug·sap·abap·alv
Wang's Blog11 小时前
前端FAQ: Vue 3 与 Vue 2 相⽐有哪些重要的改进?
前端·javascript·vue.js
再希11 小时前
React+Tailwind CSS+Shadcn UI
前端·react.js·ui
用户479492835691511 小时前
JavaScript 的 NaN !== NaN 之谜:从 CPU 指令到 IEEE 754 标准的完整解密
前端·javascript
群联云防护小杜11 小时前
国产化环境下 Web 应用如何满足等保 2.0?从 Nginx 配置到 AI 防护实战
运维·前端·nginx
醉方休12 小时前
Web3.js 全面解析
前端·javascript·electron
前端开发爱好者12 小时前
前端新玩具:Vike 发布!
前端·javascript