Vue2.X/Vue3.X项目中WangEditor 5富文本编辑器的高效封装实践:从配置到多图上传的完整指南

1. 为什么我们需要封装一个自己的WangEditor组件?

如果你在Vue项目里用过富文本编辑器,尤其是WangEditor,你肯定有过这样的体验:每次新建一个页面,都要把那一大坨配置代码复制粘贴一遍,什么工具栏配置、上传图片的接口地址、各种回调函数……更别提如果项目里有十几个地方要用到编辑器,哪天上传接口地址变了,或者要统一调整一下最大上传文件大小,你就得一个个文件去改,想想都头大。

我最早也是这么干的,直到在一个中后台项目里,表单、详情页、评论回复框加起来用了快二十个编辑器实例,维护起来简直是一场噩梦。后来我下定决心,必须把它封装成一个独立的、可复用的Vue组件。封装的好处太明显了:一处配置,处处生效逻辑集中,便于维护接口统一,调用简单。而且,一个好的封装还能帮你处理掉很多潜在的坑,比如编辑器实例的创建与销毁、Vue响应式数据与编辑器内部状态的同步、以及跨Vue版本的兼容性问题。

你可能担心,封装会不会很复杂?其实不然。WangEditor官方已经为Vue提供了很好的适配包(@wangeditor/editor-for-vue),我们要做的,就是基于它,用Vue组件化的思想,把那些零散的配置和逻辑“打包”起来,对外暴露几个简单明了的属性(props)和方法(methods)。这样一来,无论是在Vue2的老项目,还是Vue3的新项目里,你都能像使用一个普通的输入框一样,轻松地引入一个功能强大且稳定的富文本编辑器。

所以,这篇文章的目的,就是带你一步步实现这个“打包”过程。我会从最基础的安装配置讲起,重点攻克最常用也最麻烦的多图上传功能,并确保我们的封装方案能同时兼容Vue2和Vue3。最终,你会得到一个开箱即用、功能完备的 WangEditor 组件,直接复制到你的项目里就能跑起来。

2. 项目环境准备与依赖安装

万事开头先装包。这里有个小细节需要注意:Vue2和Vue3项目在安装WangEditor的Vue适配包时,版本选择上略有不同。虽然Vue3兼容大部分Vue2的语法,但底层响应式系统变了,对应的适配包版本也需要匹配。

首先,无论Vue2还是Vue3,核心的编辑器包都是同一个:

# 安装编辑器核心包
npm install @wangeditor/editor@5.1.15 --save

接下来是Vue适配包。我建议在安装前,先用 npm view 命令查看一下有哪些版本可用,避免装到不兼容的版本。

# 查看 @wangeditor/editor-for-vue 的所有版本
npm view @wangeditor/editor-for-vue versions --json

这个命令会列出一串版本号。对于 Vue2项目,你可以选择 5.1.12 这个我实测稳定的版本:

# Vue2 项目安装
npm install @wangeditor/editor-for-vue@5.1.12 --save

对于 Vue3项目,你需要安装支持Vue3的更高版本,比如 5.1.23 或以上。请务必查看官方文档或npm页面确认最新兼容版本。

# Vue3 项目安装 (版本号请以官方最新为准)
npm install @wangeditor/editor-for-vue@^5.1.23 --save

为什么这么麻烦?因为 @wangeditor/editor-for-vue 这个包内部使用了Vue的Composition API或Options API,不同版本针对Vue2和Vue3做了不同的内部实现。装错了版本,可能会导致组件无法渲染,或者响应式失效。我就在一个Vue3项目里误装了老版本,折腾了半天才发现是这里的问题。

安装完成后,你的 package.json 里应该会新增这两项依赖。接下来,我们就可以着手创建组件文件了。

3. 核心组件封装:从零搭建WangEditor.vue

封装的核心思想是:内部消化复杂配置,对外提供简洁接口。我们将创建一个 WangEditor.vue 单文件组件,它负责管理编辑器实例的生命周期、配置上传、处理内容变化等所有脏活累活。

3.1 组件模板与基本结构

我们先搭建组件的骨架。模板部分很简单,主要就是官方提供的 Toolbar(工具栏)和 Editor(编辑区域)两个组件。

<template>
  <div class="wang-editor-container">
    <!-- 工具栏区域 -->
    <Toolbar
      class="editor-toolbar"
      :editor="editorInstance"
      :defaultConfig="toolbarConfig"
    />
    <!-- 编辑区域 -->
    <Editor
      class="editor-content"
      :style="{ minHeight: editorMinHeight }"
      :defaultConfig="mergedEditorConfig"
      v-model="localContent"
      @onCreated="handleEditorCreated"
      @onChange="handleEditorChange"
    />
  </div>
</template>

<script>
// 导入Vue适配的编辑器组件
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'

export default {
  name: 'WangEditor',
  components: { Editor, Toolbar },
  props: {
    // 外部传入的配置参数
    modelValue: {
      type: String,
      default: ''
    },
    editorParams: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      editorInstance: null, // 编辑器实例,最重要的对象
      localContent: '', // 用于内部双向绑定的内容
      // 工具栏配置,可以在这里定制显示的按钮
      toolbarConfig: {
        // 例如,排除某些菜单
        // excludeKeys: ['group-video', 'emotion']
      },
      // 编辑器的默认配置,会上传配置等会合并到这里
      editorConfig: {
        placeholder: '请输入内容...',
        MENU_CONF: {} // 菜单配置,上传图片的关键在这里
      }
    }
  },
  computed: {
    // 合并默认配置和外部传入的配置
    mergedEditorConfig() {
      return {
        ...this.editorConfig,
        placeholder: this.editorParams.placeholder || this.editorConfig.placeholder,
        // 其他需要合并的配置...
      }
    },
    // 编辑器最小高度
    editorMinHeight() {
      return this.editorParams.height || '300px'
    }
  },
  watch: {
    // 监听外部传入的富文本内容变化,同步到编辑器
    modelValue(newVal) {
      if (newVal !== this.localContent && this.editorInstance) {
        this.localContent = newVal
      }
    },
    // 深度监听外部参数对象,动态更新配置(如上传地址)
    editorParams: {
      handler(newVal) {
        if (newVal && newVal.uploadImageUrl) {
          this.setupUploadConfig(newVal.uploadImageUrl)
        }
      },
      immediate: true,
      deep: true
    }
  },
  methods: {
    // 编辑器创建成功后的回调
    handleEditorCreated(editor) {
      this.editorInstance = editor
      // 这里可以做一些编辑器创建后的初始化操作
    },
    // 编辑器内容变化时的回调
    handleEditorChange(editor) {
      const html = editor.getHtml()
      // 将内容更新同步给父组件
      this.$emit('update:modelValue', html)
    }
  },
  // 组件销毁前,务必销毁编辑器实例,防止内存泄漏
  beforeUnmount() {
    if (this.editorInstance) {
      this.editorInstance.destroy()
      this.editorInstance = null
    }
  }
}
</script>

<style lang="scss" scoped>
.wang-editor-container {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  overflow: hidden;
  .editor-toolbar {
    border-bottom: 1px solid #dcdfe6;
  }
  .editor-content {
    padding: 12px;
  }
}
</style>

这个基础版本已经具备了双向绑定、参数监听和实例销毁的功能。但还缺少灵魂——图片上传。别急,我们马上来攻克它。

3.2 处理一个隐蔽的坑:拼写检查与自动聚焦

在真正开始上传功能前,我想先分享两个实际开发中遇到的“小坑”,它们不致命但很烦人。

第一个是拼写检查。浏览器默认会对可编辑区域进行英文拼写检查,在富文本编辑器里就会显示难看的红色波浪线。虽然WangEditor配置项里可以设置 spellcheck: false,但在某些浏览器(比如Chrome的某些版本)下,光配置不生效。我找到的解决办法是在组件挂载后,直接操作DOM元素来关闭它。

第二个是移动端自动聚焦。在移动设备上,如果编辑器自动获得焦点,会触发虚拟键盘弹出,影响体验。我们需要阻止这个行为。

我在组件的 mounted 生命周期里统一处理了它们:

mounted() {
  // 1. 关闭拼写检查的红色波浪线
  this.$nextTick(() => {
    const scrollElements = document.querySelectorAll('.w-e-scroll')
    scrollElements.forEach(el => {
      if (el.children[0]) {
        el.children[0].setAttribute('spellcheck', 'false')
      }
    })
  })
  // 2. 移除自动焦点,防止移动端虚拟键盘自动弹出
  if (document.activeElement && document.activeElement.blur) {
    document.activeElement.blur()
  }
}

这两个操作属于“锦上添花”的优化,能让你的编辑器体验更上一层楼。

4. 重头戏:实现强大且稳健的多图上传功能

图片上传是富文本编辑器最核心、也最容易出问题的功能。WangEditor的上传配置非常灵活,但也意味着我们需要考虑更多细节:文件大小限制、格式限制、多文件上传、上传进度、成功/失败回调、服务端返回格式约定等等。

4.1 前端上传配置详解

我们在 editorConfig.MENU_CONF 中配置 uploadImage 菜单。我将配置拆解成一个独立的方法 setupUploadConfig,这样逻辑更清晰。

methods: {
  setupUploadConfig(uploadUrl) {
    // 确保 MENU_CONF 存在
    if (!this.editorConfig.MENU_CONF) {
      this.editorConfig.MENU_CONF = {}
    }
    this.editorConfig.MENU_CONF['uploadImage'] = {
      // ------------ 基础配置 ------------
      server: uploadUrl, // 上传接口地址,由父组件传入
      fieldName: 'files', // 表单文件字段名,与服务端接收参数名对应
      maxFileSize: 2 * 1024 * 1024, // 单个文件最大 2MB
      maxNumberOfFiles: 10, // 一次最多选10个文件
      allowedFileTypes: ['image/*'], // 允许所有图片类型
      timeout: 10000, // 10秒超时
      withCredentials: false, // 跨域是否发送cookie,根据你的项目设置

      // ------------ 自定义参数与请求头 ------------
      meta: {
        // 这里可以传递一些额外的认证信息,比如token
        token: this.getAuthToken(),
        from: 'wangeditor'
      },
      headers: {
        // 设置自定义请求头
        'X-Requested-With': 'XMLHttpRequest'
      },

      // ------------ 生命周期回调函数 ------------
      // 选择文件后,上传之前触发。可以在这里做最后校验或修改文件信息。
      onBeforeUpload(file) {
        console.log(`${file.name} 开始上传`)
        // 如果返回 false,会阻止上传
        // 如果返回 File 对象,会使用返回的这个新对象
        return file
      },

      // 上传进度回调,可以用来做进度条
      onProgress(progress) {
        // progress 是一个 0-100 的数字
        console.log('当前进度:', progress)
      },

      // 单个文件上传成功
      onSuccess(file, res) {
        // 重点:这里需要根据你服务端返回的实际数据结构来解析!
        // WangEditor 期望的返回值格式是 { errno: 0, data: { url: '图片地址' } }
        console.log(`${file.name} 上传成功`, res)
        // 通常你不需要在这里做任何事,编辑器会自动处理。
        // 但如果服务端返回格式不一致,你可能需要在这里转换格式。
      },

      // 单个文件上传失败
      onFailed(file, res) {
        console.error(`${file.name} 上传失败`, res)
        // 可以在这里触发全局错误提示
        this.$message.error(`文件 ${file.name} 上传失败`)
      },

      // 上传过程发生错误(网络错误、超时等)
      onError(file, err, res) {
        console.error(`${file.name} 上传出错`, err, res)
        this.$message.error('网络错误,请重试')
      }
    }
  }
}

这里有几个关键点需要你根据实际情况调整:

  1. fieldName:必须和服务端接收文件参数的字段名一致。我习惯用 files,也有人用 fileimages
  2. maxFileSizeallowedFileTypes:这是前端的第一道防线,可以有效拦截不合法文件,减轻服务端压力。
  3. onSuccess 回调:这是最容易出错的地方。WangEditor要求服务端返回一个非常特定的JSON格式:{ errno: 0, data: { url: 'https://xxx.com/image.jpg' } }errno 必须是数字 0 表示成功,data 是一个对象,里面至少包含 url 字段。如果你的后端同事返回的格式不一样(比如 { code: 200, url: '...' }),你就必须在 onSuccess 回调里手动转换成编辑器能识别的格式。

4.2 服务端实现(Java Spring Boot示例)

为了让你前后端联调更顺畅,这里给出一个完整的Spring Boot控制器示例。它接收多文件,保存到本地,并返回WangEditor期望的格式。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.*;

@RestController
@RequestMapping("/api")
public class FileUploadController {

    @Value("${app.upload.dir:/tmp/uploads}") // 从配置文件中读取上传路径
    private String uploadDir;

    @PostMapping("/upload/image")
    // @CrossOrigin // 如果前端是跨域请求,需要添加此注解或使用全局配置
    public Map<String, Object> uploadImage(@RequestParam("files") List<MultipartFile> files) {
        Map<String, Object> result = new HashMap<>();
        List<Map<String, String>> dataList = new ArrayList<>();

        try {
            for (MultipartFile file : files) {
                // 1. 生成唯一文件名,防止覆盖
                String originalFilename = file.getOriginalFilename();
                String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
                String newFilename = UUID.randomUUID().toString().replace("-", "") + fileExtension;

                // 2. 确定文件保存路径
                File destDir = new File(uploadDir);
                if (!destDir.exists()) {
                    destDir.mkdirs();
                }
                File destFile = new File(destDir, newFilename);

                // 3. 保存文件
                file.transferTo(destFile);

                // 4. 构造返回给前端的数据项
                // 假设你的静态资源可以通过 http://your-domain.com/uploads/ 访问
                String fileUrl = "/uploads/" + newFilename; // 或者完整的绝对URL
                Map<String, String> fileInfo = new HashMap<>();
                fileInfo.put("url", fileUrl);
                fileInfo.put("alt", originalFilename); // 可选
                fileInfo.put("href", ""); // 可选,图片链接
                dataList.add(fileInfo);
            }

            // 5. 严格按照WangEditor要求的格式返回
            result.put("errno", 0); // 关键!必须是数字0
            result.put("data", dataList);

        } catch (IOException e) {
            e.printStackTrace();
            result.put("errno", 1); // 非0即表示失败
            result.put("message", "文件上传失败");
        }

        return result;
    }
}

对应的,你需要在 application.yml 中配置上传路径,并设置静态资源映射,让保存的图片能被外部访问到。

# application.yml
app:
  upload:
    dir: D:/project_uploads/ # Windows路径示例,Linux/Mac请修改

# 静态资源映射配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Value("${app.upload.dir}")
    private String uploadDir;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 将 /uploads/** 路径映射到本地文件目录
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:" + uploadDir + File.separator);
    }
}

这样,一个支持多图上传、带基础错误处理的后端接口就准备好了。前端组件里的 uploadImageUrl 就可以配置为 http://你的域名/api/upload/image

5. 在父组件中轻松调用与进阶技巧

封装好后,在父组件中使用就变得极其简单。

5.1 基础使用

<template>
  <div>
    <h3>发布文章</h3>
    <WangEditor
      v-model="articleContent"
      :editor-params="editorConfig"
    />
    <button @click="submit">提交</button>
  </div>
</template>

<script>
import WangEditor from '@/components/WangEditor.vue'

export default {
  components: { WangEditor },
  data() {
    return {
      articleContent: '<p>这里是初始内容</p>',
      editorConfig: {
        placeholder: '请输入正文...',
        uploadImageUrl: '/api/upload/image', // 上传地址
        height: '500px'
      }
    }
  },
  methods: {
    submit() {
      console.log('提交的HTML内容:', this.articleContent)
      // 这里可以发送到后端
    }
  }
}
</script>

通过 v-model 就能轻松获取和设置富文本内容,所有复杂的上传逻辑都隐藏在组件内部。

5.2 进阶:通过Ref调用组件方法

有时我们可能需要以编程方式操作编辑器,比如清空内容、插入特定内容、或者获取纯文本。我们可以在子组件中暴露一些方法,然后通过 ref 来调用。

WangEditor.vue 组件中,我们添加以下方法:

methods: {
  // ... 其他方法
  // 获取纯文本
  getPlainText() {
    return this.editorInstance ? this.editorInstance.getText() : ''
  },
  // 获取HTML
  getHtml() {
    return this.editorInstance ? this.editorInstance.getHtml() : ''
  },
  // 清空内容
  clear() {
    if (this.editorInstance) {
      this.editorInstance.clear()
      this.localContent = ''
    }
  },
  // 插入HTML
  insertHtml(html) {
    if (this.editorInstance) {
      const editor = this.editorInstance
      editor.restoreSelection() // 恢复选区
      editor.insertHtml(html)
    }
  }
}

在父组件中,就可以这样使用:

<template>
  <div>
    <WangEditor ref="myEditor" v-model="content" :editor-params="config"/>
    <button @click="handleGetText">获取纯文本</button>
    <button @click="handleClear">清空</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleGetText() {
      const text = this.$refs.myEditor.getPlainText()
      console.log('纯文本:', text)
    },
    handleClear() {
      this.$refs.myEditor.clear()
    }
  }
}
</script>

5.3 性能优化:使用v-if控制显隐

最后是一个非常重要的性能优化点。如果你的编辑器组件在一个弹窗或标签页里,会频繁地创建和销毁,务必使用 v-if 而不是 v-show

因为WangEditor实例包含大量的DOM操作和事件监听,单纯的 v-show(隐藏)无法销毁实例,会造成内存泄漏。而 v-if 在条件为假时,会完全销毁组件及其子组件。

<template>
  <div>
    <button @click="showEditor = !showEditor">切换编辑器</button>
    <!-- 使用 v-if 确保彻底销毁 -->
    <WangEditor v-if="showEditor" v-model="content" :editor-params="config"/>
  </div>
</template>

这个坑是我在开发一个复杂标签页应用时踩到的,页面打开久了就会越来越卡,排查后发现就是因为编辑器实例没有正确销毁。改用 v-if 后问题迎刃而解。

6. 跨版本兼容与样式自定义

我们的封装方案天生就具备Vue2/Vue3兼容性。核心在于两点:一是我们使用了Vue2风格的Options API编写组件,这在Vue3中是完全被兼容的(除非你项目明确禁用);二是我们通过 @wangeditor/editor-for-vue 这个适配包来屏蔽底层差异。

对于Vue3项目,你只需要确保安装正确版本的适配包(如前面所述),然后使用方式完全一样。如果你的Vue3项目全面采用Composition API和 <script setup>,你可能希望组件也以同样的方式提供。这也很简单,你可以基于现有的逻辑,用Composition API重写一遍 WangEditor.vue,或者提供一个额外的适配版本。但就我的经验而言,在大多数混合项目中,当前这个基于Options API的组件已经足够好用且稳定。

关于样式,你可以通过覆盖CSS类名来深度定制编辑器的外观。组件的 <style> 部分已经给出了一些基础样式,比如边框、圆角。WangEditor有自己的一套CSS类名体系,例如 .w-e-text-container 是编辑区,.w-e-toolbar 是工具栏。你可以通过浏览器开发者工具查看元素类名,然后在你项目的全局样式文件或组件的scoped样式中进行覆盖。比如,想修改工具栏按钮的悬停颜色,可以这样写:

/* 在全局样式或组件内(非scoped下) */
.w-e-toolbar .w-e-menu:hover {
  background-color: #f0f7ff;
}

封装和调试的过程,其实就是不断与编辑器“对话”的过程。遇到问题,多看看官方文档,多利用浏览器控制台查看网络请求和错误信息,大部分问题都能找到答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值