Vue 项目集成 wangEditor 富文本编辑器实战与避坑指南
1. 为什么在 Vue 项目里选富文本编辑器是个技术活做过后台管理系统的朋友大概率都遇到过这个需求运营同学跑过来说文章编辑页那个纯文本框太难用了标题加粗、插入图片、调个字号都得手写 HTML能不能给换成那种所见即所得的编辑器。这个需求听起来简单但真动手选型的时候你会发现市面上的富文本编辑器多如牛毛Quill、TinyMCE、CKEditor、wangEditor 各有各的脾气选错了后面全是坑。我自己在好几个 Vue 项目里都用过 wangEditor从早期的 3.x 版本一路用到现在的 5.x踩过的坑能写满一页纸。这篇文章就把我在 Vue 项目中使用 wangEditor 的完整经验梳理一遍包括为什么选它、怎么集成、图片上传怎么配、样式冲突怎么解决、光标丢失怎么排查这些实际问题。如果你正在做后台内容管理、社区发帖、商品详情编辑这类功能或者你已经被某个编辑器的坑折磨得够呛那这篇内容应该能帮你省下不少时间。wangEditor 是一个国产的开源富文本编辑器特点是轻量、API 设计对国内开发者友好、文档是中文的、开箱即用的功能比较全。它不像 CKEditor 那样庞大也不像 Quill 那样需要你对它的数据模型有深入理解才能玩转。对于大部分中小型项目来说wangEditor 在功能覆盖和接入成本之间找到了一个不错的平衡点。但平衡归平衡实际用起来还是有不少细节需要处理尤其是和 Vue 的响应式系统配合的时候稍不注意就会出问题。2. 版本选型与项目环境准备2.1 wangEditor 版本差异与选择建议wangEditor 目前主流的是 5.x 版本和 4.x 相比变化非常大。4.x 是基于 textarea 的封装思路而 5.x 是完全重写的采用了类似 Slate 的数据模型底层用 contenteditable 实现API 也完全不一样。如果你在网上搜到的教程用的是new wangEditor()这种写法那大概率是 4.x 的直接套到 5.x 上会报错。5.x 的核心 API 变成了createEditor和createToolbar两个工厂函数配置项通过config对象传入。这个设计比 4.x 更灵活但也意味着你需要重新熟悉一遍文档。我的建议是新项目直接上 5.x不要因为网上 4.x 的教程多就选旧版本5.x 在图片上传、自定义菜单、粘贴处理这些方面都成熟很多。安装很简单用 npm 或者 yarn 都行npm install wangeditor/editor wangeditor/editor-for-vue注意这里装了两个包wangeditor/editor是核心包wangeditor/editor-for-vue是 Vue 的封装组件。如果你用的是 Vue 3要确认装的editor-for-vue版本是支持 Vue 3 的目前wangeditor/editor-for-vue5.x同时支持 Vue 2 和 Vue 3但引入方式略有不同。2.2 Vue 2 与 Vue 3 的引入差异Vue 3 项目里这样引入import { Editor, Toolbar } from wangeditor/editor-for-vueVue 2 项目里需要这样import { Editor, Toolbar } from wangeditor/editor-for-vue/dist/index.esm.js这个差异是因为包的入口文件对 Vue 2 和 Vue 3 做了区分如果你在 Vue 2 项目里用了 Vue 3 的引入方式会报组件未注册或者渲染异常的错误。我第一次在 Vue 2 项目里用的时候就被这个坑了一下控制台报了一堆看不懂的警告后来翻文档才发现是引入路径的问题。另外wangEditor 的样式文件需要单独引入import wangeditor/editor/dist/css/style.css这个 CSS 文件包含了编辑器的主体样式和工具栏样式不引入的话编辑器会显示得乱七八糟工具栏按钮全部挤在一起编辑区域也没有边框。2.3 基础集成的完整代码结构在 Vue 3 的script setup里一个最基础的集成大概长这样template div styleborder: 1px solid #ccc Toolbar :editoreditorRef :defaultConfigtoolbarConfig modedefault styleborder-bottom: 1px solid #ccc / Editor v-modelvalueHtml :defaultConfigeditorConfig modedefault styleheight: 400px; overflow-y: hidden onCreatedhandleCreated / /div /template script setup import { ref, shallowRef, onBeforeUnmount } from vue import { Editor, Toolbar } from wangeditor/editor-for-vue import wangeditor/editor/dist/css/style.css const editorRef shallowRef() const valueHtml ref(p请输入内容.../p) const toolbarConfig {} const editorConfig { placeholder: 请输入内容... } const handleCreated (editor) { editorRef.value editor } onBeforeUnmount(() { const editor editorRef.value if (editor null) return editor.destroy() }) /script这段代码里有几个关键点需要特别注意。第一editorRef必须用shallowRef而不是ref因为 wangEditor 的实例是一个复杂的对象如果用ref包裹Vue 会尝试对它进行深度响应式代理这会导致编辑器内部状态混乱出现光标跳动、输入延迟甚至崩溃的问题。这个坑非常隐蔽因为你不一定会立刻发现异常但用着用着就会出各种奇怪的问题。第二onBeforeUnmount里必须调用editor.destroy()否则编辑器实例不会被正确销毁在单页应用里切换路由时会内存泄漏反复进入编辑页面几次之后浏览器就会变得很卡。第三v-model绑定的valueHtml是 HTML 字符串编辑器的内容变化会同步到这个变量上。但要注意这个同步不是实时的wangEditor 内部做了防抖处理如果你需要在用户输入时立刻获取最新内容不能依赖valueHtml而应该通过editor.getHtml()方法主动获取。3. 核心功能配置与图片上传实战3.1 工具栏菜单的定制与裁剪wangEditor 默认的工具栏菜单非常全从标题、加粗、斜体到表格、代码块、视频都有。但实际项目里往往不需要这么多功能比如一个简单的公告编辑场景可能只需要加粗、斜体和列表就够了。这时候就需要通过toolbarConfig来裁剪菜单。const toolbarConfig { excludeKeys: [ group-video, insertTable, codeBlock, fullScreen ] }excludeKeys是排除法把不想要的菜单 key 列进去就行。菜单的 key 可以在官方文档的菜单配置页查到每个菜单都有一个唯一的 key。如果你想用包含法可以用toolbarKeys来指定只显示哪些菜单但要注意分组菜单的 key 和子菜单的 key 不一样比如group-image是图片分组里面包含insertImage、uploadImage等子菜单。我个人的经验是对于大多数后台管理场景保留标题、加粗、斜体、下划线、删除线、字体颜色、背景色、有序列表、无序列表、对齐方式、链接、图片上传、撤销重做这些就够了。表格和视频功能虽然强大但配置起来麻烦而且用户实际使用频率很低不如砍掉让界面更清爽。3.2 图片上传的完整配置方案图片上传是富文本编辑器里最容易出问题的环节。wangEditor 默认的图片上传是把图片转成 base64 直接塞进 HTML 里这在正式项目里肯定不能用因为 base64 字符串会让内容体积暴增数据库存储和传输都会受影响。必须配置成上传到自己的服务器或者对象存储。wangEditor 5.x 的图片上传配置有两种方式一种是customUpload完全自己实现上传逻辑另一种是menuConf配合uploadImage配置。我推荐用customUpload因为控制权完全在自己手里想怎么传就怎么传。const editorConfig { placeholder: 请输入内容..., MENU_CONF: { uploadImage: { customUpload(file, insertFn) { const formData new FormData() formData.append(file, file) // 调用你自己的上传接口 axios.post(/api/upload, formData, { headers: { Content-Type: multipart/form-data } }).then(res { const url res.data.url insertFn(url, file.name, url) }).catch(err { console.error(图片上传失败, err) }) } } } }这里的insertFn是 wangEditor 提供的回调接收三个参数图片地址、alt 文本、链接地址。调用insertFn之后编辑器会自动在光标位置插入图片。注意customUpload是一个同步函数但内部可以执行异步操作只要在异步完成后调用insertFn就行。有一个细节需要注意customUpload的file参数是File对象但 wangEditor 在调用之前已经对图片做了压缩处理如果开启了压缩配置。默认情况下wangEditor 会把超过一定尺寸的图片压缩后再上传这个行为可以通过compressSize和maxSize来调整。如果你不希望编辑器压缩图片可以把compressSize设为 0。3.3 图片上传的踩坑记录我在图片上传这块踩过几个比较典型的坑这里分享一下。第一个坑是上传接口返回的数据结构。wangEditor 的insertFn需要的是图片的完整 URL但很多后端接口返回的是相对路径比如/uploads/2024/01/abc.jpg。如果你直接把相对路径传给insertFn图片在编辑器里能显示因为浏览器会基于当前域名拼接但保存到数据库后再在其他地方渲染时如果域名变了或者前端部署在不同的子路径下图片就会 404。正确的做法是在上传成功后拼接完整的域名前缀或者让后端直接返回完整 URL。第二个坑是上传失败的处理。customUpload里如果上传失败你需要在 catch 里给用户一个提示否则用户会以为图片上传成功了但看不到图片。wangEditor 本身不会自动处理上传失败的 UI 反馈这部分需要自己实现。我一般会在 catch 里调用一个全局的 message 组件弹出错误提示。第三个坑是并发上传。wangEditor 支持一次选择多张图片它会为每张图片分别调用customUpload。如果你的上传接口不支持并发或者后端有并发限制就需要自己加一个队列来控制。我遇到过一次用户一次性选了 20 张图片结果后端接口被并发请求打挂的情况后来加了一个简单的 Promise 队列才解决。4. 样式冲突与响应式数据的那些坑4.1 编辑器样式被全局样式污染这个问题在使用了 UI 组件库的项目里特别常见。比如你用了某个组件库它可能对p、h1到h6、ul、ol这些标签设置了全局样式而 wangEditor 的编辑区域内部也是用这些标签来渲染内容的结果就是编辑器里的内容和外部页面的样式混在一起显示效果完全不对。解决思路有两个方向。第一个方向是给编辑器容器加一个高优先级的样式隔离比如用 scoped 样式加上:deep()选择器来覆盖.editor-wrapper :deep(.w-e-text-container) p { margin: 0; line-height: 1.6; }第二个方向是在编辑器初始化时通过editorConfig的contentStyle来注入内联样式这样样式会直接作用在编辑区域的内容上优先级最高const editorConfig { contentStyle: { fontSize: 14px, lineHeight: 1.6, color: #333 } }我一般两个方向同时用contentStyle保证基础样式scoped 样式处理特殊情况。但要注意contentStyle只影响编辑区域的显示不影响最终保存的 HTML 内容。如果你希望保存的 HTML 在其他地方渲染时也有一致的样式需要在渲染端也做相应的样式处理。4.2 v-model 双向绑定的性能问题前面提到v-model绑定的 HTML 字符串不是实时同步的wangEditor 内部做了防抖。但即使有防抖在内容很长的时候每次同步都会触发 Vue 的响应式更新如果这个 HTML 字符串还被其他计算属性或者侦听器依赖就可能引发连锁更新导致输入卡顿。我遇到过一个极端情况编辑页面的valueHtml被一个watch监听每次变化都去调用一个接口做草稿保存。结果用户每输入几个字就发一次请求不仅浪费带宽还导致输入框偶尔卡住。后来改成手动控制保存频率用setInterval或者用户主动点击保存按钮才触发问题就解决了。另外如果你不需要把编辑器内容实时同步到 Vue 的响应式系统里可以考虑不用v-model而是通过onChange事件手动获取内容const handleChange (editor) { // 这里可以做一些轻量级的处理比如更新字数统计 // 但不要直接赋值给响应式变量除非确实需要 }4.3 编辑器实例与 Vue 响应式系统的隔离再强调一遍shallowRef的重要性。wangEditor 的实例上挂载了大量的内部状态和方法如果被 Vue 的reactive或ref深度代理会出现一些难以排查的问题。我遇到过的一个典型症状是编辑器初始化后一切正常但切换几次路由再回来编辑器就变成了只读状态无法输入。排查了很久才发现是因为实例被响应式代理后内部的状态判断出了问题。除了用shallowRef还要注意不要把编辑器实例挂载到reactive对象的属性上。比如// 错误做法 const state reactive({ editor: null }) // 正确做法 const editorRef shallowRef(null)这个细节在官方文档里没有特别强调但实际项目中非常关键。5. 常见问题排查与独家避坑指南5.1 编辑器不显示或显示异常的排查思路编辑器不显示是最常见的问题可能的原因有好几种。第一样式文件没有引入这时候编辑器会渲染出来但完全没有样式工具栏按钮堆在一起。第二容器没有设置高度wangEditor 的编辑区域默认没有高度如果父容器也没有高度编辑器就会塌缩成一条线。第三v-model绑定的变量初始值不是字符串比如绑了一个null或者undefined编辑器初始化时会报错。排查的时候可以按这个顺序来先看控制台有没有报错再看样式文件是否引入然后检查容器高度最后检查绑定值。我一般会在编辑器外面包一个固定高度的 div确保编辑器有足够的显示空间。5.2 光标丢失与输入法兼容问题光标丢失是 contenteditable 类编辑器的通病wangEditor 也不例外。常见的场景是用户在输入中文的时候输入法候选框还没消失编辑器内容被外部程序更新了导致光标位置重置。这个问题在 Vue 的响应式更新和编辑器内部更新同时发生时特别容易出现。缓解这个问题的办法是尽量减少外部对编辑器内容的直接修改。如果你需要从外部设置编辑器内容不要直接改v-model绑定的变量而是调用编辑器的 APIeditorRef.value.setHtml(p新的内容/p)另外在输入法组合输入期间compositionstart 到 compositionend 之间尽量避免任何可能触发编辑器重渲染的操作。wangEditor 内部对输入法做了一些处理但如果你在onChange里做了比较重的操作还是可能干扰输入。5.3 粘贴内容的清洗与格式控制用户从 Word 或者网页上复制内容粘贴到编辑器里往往会带进来一堆乱七八糟的样式和标签。wangEditor 默认会保留一部分格式但有时候保留的格式并不是我们想要的。比如从网页复制的内容可能带有背景色、特殊字体、外链图片等这些内容保存到数据库后在前端渲染时可能会破坏页面布局。wangEditor 提供了pasteFilterStyle和pasteIgnoreImg等配置来控制粘贴行为const editorConfig { pasteFilterStyle: true, // 过滤粘贴内容的样式 pasteIgnoreImg: true, // 忽略粘贴内容中的图片 }如果这些内置配置不够用还可以通过onPaste钩子完全自定义粘贴处理逻辑。我一般会开启pasteFilterStyle然后对粘贴的图片做特殊处理因为用户从网页复制的图片往往是外链直接保存外链图片存在失效的风险最好是拦截下来上传到自己的服务器。5.4 常见问题速查表问题现象可能原因解决方向编辑器不显示样式未引入或容器无高度引入 CSS 文件设置容器高度输入卡顿实例被深度响应式代理使用 shallowRef 包裹实例图片上传后不显示返回的是相对路径拼接完整域名或让后端返回完整 URL切换路由后编辑器只读实例未销毁或重复初始化onBeforeUnmount 中调用 destroy粘贴内容样式混乱未开启样式过滤配置 pasteFilterStyle 或自定义 onPaste光标跳到开头外部修改了绑定值用 editor.setHtml 代替直接改变量工具栏按钮不显示菜单 key 配置错误检查 excludeKeys 或 toolbarKeys编辑器内容为空时 v-model 为 null初始值未设置给 v-model 绑定变量设初始空字符串6. 一些进阶玩法与个人经验总结6.1 自定义菜单的扩展思路wangEditor 5.x 支持自定义菜单虽然文档里写得比较简略但实际用起来并不复杂。核心思路是继承Boot类实现getValue、isActive、exec等方法然后通过toolbarConfig注册进去。我做过一个插入商品卡片的自定义菜单点击后弹出一个商品选择器选完后在编辑器里插入一个带有商品信息的卡片 HTML。这个功能用自定义菜单实现起来很顺畅比在编辑器外面加按钮再手动插入内容要优雅得多。6.2 内容回显与只读模式编辑器的只读模式通过mode属性控制设为simple就是只读。但要注意只读模式下工具栏也应该隐藏或者禁用否则用户点击工具栏按钮不会有任何反应体验很奇怪。我一般会在只读模式下把整个工具栏隐藏掉只保留内容展示区域。内容回显的时候如果 HTML 字符串比较大编辑器的初始化会有一个明显的延迟。优化办法是先用一个 loading 占位等编辑器onCreated之后再设置内容。不要在编辑器还没创建完成的时候就试图设置内容那样不会生效。6.3 我个人在实际项目中的几点体会第一wangEditor 的文档虽然比较全但有些细节需要自己试才能搞清楚。比如customUpload的insertFn到底接收几个参数文档里写的是三个但实际测试发现 alt 和链接参数在某些版本里是可选的。遇到不确定的地方直接看源码或者写个 demo 试一下比反复翻文档快。第二编辑器的版本升级要谨慎。5.x 的小版本之间偶尔会有一些不兼容的改动升级前最好先看一下 changelog在测试环境验证一遍再上生产。我有一次直接升级了 minor 版本结果图片上传的配置项名字变了线上图片上传直接挂了排查了半天。第三对于内容安全要求比较高的场景不要完全依赖编辑器的过滤功能。wangEditor 的粘贴过滤和 XSS 防护有一定作用但最可靠的做法还是在后端保存内容之前再做一次清洗把不允许的标签和属性过滤掉。前端编辑器负责用户体验后端负责安全兜底这个分工不能乱。第四如果项目里有多处用到富文本编辑器建议封装成一个通用的组件把图片上传、菜单配置、样式隔离这些逻辑都收敛进去。这样后续要换编辑器或者调整配置只需要改一个地方不用满项目找。我现在的做法是封装一个RichEditor组件对外暴露v-model和几个常用的 props内部处理所有 wangEditor 的细节业务页面用起来就很简单了。这个内容后续还可以这样扩展如果你需要支持 Markdown 语法可以在编辑器外面加一个 Markdown 预览面板用editor.getText()获取纯文本内容再做解析如果需要多人协作编辑那就需要考虑换一个支持 OT 或 CRDT 算法的编辑器方案wangEditor 本身不提供协作能力。但那是另一个话题了对于大多数单用户编辑场景wangEditor 已经足够好用。