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>
相关推荐
闲蛋小超人笑嘻嘻10 分钟前
前端面试三之控制语句
前端
袁煦丞10 分钟前
【跨平台抓取音乐秒变MP3】PlaylistDL破解下载自由:cpolar内网穿透实验室第501个成功挑战
前端·程序员·远程工作
一只猫猫熊30 分钟前
密码强度、一致性、相似度校验
前端
木林90730 分钟前
使用Vue3开发商品管理器(一)
前端
孤蓬&听雨33 分钟前
Axure高保真LayUI框架 V2.6.8元件库
前端·layui·产品经理·axure·原型设计
Hilaku34 分钟前
为什么我放弃使用 Pinia?
前端·javascript·vue.js
yrjw38 分钟前
使用ReactNative加载Svga动画支持三端【android/ios/harmony】
前端
贩卖纯净水.39 分钟前
webpack继续学习
前端·学习·webpack
陈_杨39 分钟前
鸿蒙5开发宝藏案例分享---在线短视频流畅切换
前端·javascript
Smile_frank43 分钟前
vue-06(“$emit”和事件修饰符)
前端