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>
相关推荐
zwjapple35 分钟前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20203 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem3 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊4 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术4 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing4 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止4 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall4 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴4 小时前
简单入门Python装饰器
前端·python
袁煦丞5 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作