ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue3项目集成wangEditor富文本编辑器实践与优化

Vue3项目集成wangEditor富文本编辑器实践与优化 1. 富文本编辑器选型为什么在 Vue3 项目里我最终留下了 wangEditor做后台管理系统这么多年富文本编辑器这个坑我踩过不止一轮。从最早的 UEditor到后来的 Quill、TinyMCE、CKEditor再到国内的 wangEditor几乎每一款都在项目里跑过一段时间。每次新起一个 Vue3 项目需求方一句内容编辑要能加粗、能插图、能贴表格就意味着又要在选型上做一次权衡。这篇就聊聊我在 Vue3 里用 wangEditor 的完整实践包括踩过的坑和最后稳定下来的方案。先说结论wangEditor 是我在中后台内容编辑场景下投入产出比最高的选择。它不是一个大而全的编辑器但对国内团队来说很多默认行为是对味的——中文文档全、开箱即用、体积可控、和 Vue3 的组件化思路能接得上。它不是没有短板后面我会专门讲哪些场景别用它。1.1 中后台场景对编辑器的真实诉求很多人选编辑器喜欢看功能列表觉得功能越多越好。实际做过项目就知道功能越多的编辑器集成成本、样式冲突、打包体积、升级风险全都跟着涨。我在中后台项目里总结出来的诉求其实是下面这几条按优先级排能跑起来且不崩编辑区初始化快不卡输入粘贴大段文字不假死。富文本能力够用标题、加粗、斜体、列表、引用、代码块、链接、图片上传、表格。图片上传可控能接自己的后端接口而不是只能用官方云服务。HTML 输出干净保存到数据库的内容第二次打开不会因为标签混乱而错位。支持只读模式详情页要能展示不能只是可编辑态。体积别太夸张后台管理系统首屏已经够重了编辑器再塞几百 KB 就很难交代。wangEditor 基本都覆盖了。它默认的工具栏配置和中文语境贴合比如标题引用分割线这些用起来不需要翻译脑回路。更关键的是它的核心包不算重按需引入之后对首屏的影响在可接受范围内。1.2 和 Quill、TinyMCE 相比wangEditor 的位置在哪我不喜欢空谈哪个最好因为脱离场景的选择都是耍流氓。下面这张表是我自己在几个项目里对比后留下的印象仅供参照具体还是看你的需求维度wangEditorQuillTinyMCE中文文档与社区中文文档完整国内资料多英文为主中文资料一般文档全但偏英文开箱即用程度高默认配置就够用中工具栏要自己装配高但商业授权需注意图片上传自定义上传接口简单需自定义 handler需配置 upload体积相对轻量较轻偏重定制成本中低中高中适合场景国内中后台内容编辑轻量编辑、博客企业级复杂文档提示TinyMCE 的授权模式需要你在选型阶段就确认清楚别等上线前才发现合规问题。中后台项目如果只是做内容录入wangEditor 和 Quill 基本都能扛住。我最终选 wangEditor一个很现实的原因团队里新人上手快。中文文档意味着查问题的时间成本低遇到一个不认识的配置项翻文档就能找到不用去英文 issue 里大海捞针。这在人员流动比较频繁的团队里是个隐形但很实在的优势。1.3 一个容易被忽略的点编辑器的数据主权还有一件事值得单独说。编辑器里编辑的内容最终是要保存到你自己数据库的。如果编辑器把内容格式绑架得太死你后面想换编辑器、想做内容迁移就会非常痛苦。wangEditor 输出的是标准 HTML这一点对数据主权很友好。我见过一些编辑器输出的是自己一套私有 JSON 结构结果导数据的时候一堆麻烦。用 wangEditor 的话存库就是一段 HTML 字符串想用后端做内容解析、敏感词过滤、SEO 渲染都还有操作空间。这也是我倾向它的原因之一。2. 环境准备与依赖版本先把地基打稳切到实操。Vue3 项目里集成 wangEditor第一步不是写代码而是把依赖和版本关系理清。这一步做不好后面会出现各种莫名其妙的报错而且报错信息往往指向不清排查起来非常耗时间。2.1 不同版本混用是最常见的报错源头wangEditor 目前主流有两代4.x 和 5.x。这两代的 API 差异非常大你把 4.x 的教程代码复制到 5.x 的环境里必然报错。我见过不少人照着老博客写然后卡在 createEditor is not a function 上半天。所以第一件事确认你的版本。安装的时候明确指定# 安装 5.x 版本推荐 npm install wangeditor/editor wangeditor/editor-for-vuenext # 如果项目是 vue2 环境对应的是 npm install wangeditor/editor wangeditor/editor-for-vue注意那个next它是区分 Vue3 和 Vue2 封装包的关键。Vue3 用的是wangeditor/editor-for-vuenextVue2 用的是不带next的版本。这个细节如果搞混组件注册就会失败。我的习惯是装完之后在package.json里核对一下{ dependencies: { wangeditor/editor: ^5.1.23, wangeditor/editor-for-vue: ^5.1.12 } }两个包的版本号不一定一致但只要都在 5.x 系列就是兼容的。2.2 用 Vite 还是 Webpack样式引入的差异现在新建 Vue3 项目大多用 Vite。Vite 和 Webpack 在处理 CSS 引入时有些细微差别特别是 wangEditor 需要引入一份基础样式。我的做法是在组件里直接引import wangeditor/editor/dist/css/style.css这行一定要加而且要在组件加载的时候加。漏掉这行编辑器能渲染但工具栏会变成一堆没有样式的纯文本按钮看起来像是坏掉了。这个坑我踩过一次当时以为是版本问题折腾了半小时才发现是样式没引。如果你是旧项目用 Webpack同样的写法一般也没问题。但如果遇到样式被全局 reset 覆盖可以在组件外层加个作用域容器把编辑器的容器限定在一定范围内减少全局样式冲突。2.3 Vue3 组合式 API 下的组件注册方式wangeditor/editor-for-vue提供的是Editor和Toolbar两个组件。在 Vue3 里我倾向于在使用它的组件内部按需注册而不是全局挂import { Editor, Toolbar } from wangeditor/editor-for-vue export default { components: { Editor, Toolbar } }如果你用script setup那就更简单直接 import 后在模板里用Editor和Toolbar就行。这里要注意组件名的大小写模板里用的是首字母大写形式和 import 的名字保持一致最省事。注意不要在多个组件里重复全局注册 wangEditor容易造成实例混乱。哪个组件用就在哪个组件引入。3. 从零搭起一个可用的编辑器组件这一节是核心。我会给出一个完整的、可以直接抄进项目里的编辑器组件然后逐段解释为什么这么写。别急着复制先看逻辑不然出了问题你都不知道从哪查。3.1 组件骨架Toolbar 与 Editor 的配合关系wangEditor 5.x 的组件设计是Toolbar负责工具栏Editor负责编辑区。两者通过同一个editor实例绑定。这个设计很清晰因为你有时候希望工具栏固定在顶部而编辑区滚动分开就很好控制。先看一个完整的组件结构template div classeditor-wrap Toolbar classeditor-toolbar :editoreditorRef :defaultConfigtoolbarConfig :modemode / Editor classeditor-body v-modelvalueHtml :defaultConfigeditorConfig :modemode onCreatedhandleCreated onChangehandleChange / /div /template script setup import { ref, shallowRef, onBeforeUnmount, watch } from vue import wangeditor/editor/dist/css/style.css import { Editor, Toolbar } from wangeditor/editor-for-vue const props defineProps({ modelValue: { type: String, default: } }) const emit defineEmits([update:modelValue]) const editorRef shallowRef() const valueHtml ref(props.modelValue) const mode default const toolbarConfig {} const editorConfig { placeholder: 请输入内容..., MENU_CONF: {} } const handleCreated (editor) { editorRef.value editor } const handleChange () { emit(update:modelValue, valueHtml.value) } onBeforeUnmount(() { const editor editorRef.value if (editor) editor.destroy() }) /script style scoped .editor-wrap { border: 1px solid #dcdfe6; border-radius: 4px; overflow: hidden; } .editor-toolbar { border-bottom: 1px solid #dcdfe6; } .editor-body { height: 400px; overflow-y: auto; } /style这个骨架里有几个关键点必须说清楚。3.2 为什么 editorRef 要用 shallowRef 而不是 ref这是我最想强调的一个点很多人直接用ref存 editor 实例然后遇到各种诡异问题。原因在于wangEditor 的 editor 实例是一个非常复杂的对象内部有大量嵌套引用、DOM 节点、循环引用。如果你用ref()包它Vue3 会尝试把它变成响应式代理也就是递归地给这个对象的每个属性都套一层 Proxy。这个过程不仅性能差还可能导致编辑器内部的状态错乱——比如某些内部对象被代理后方法里的this指向变了行为就不对了。shallowRef只对.value这一层的替换做响应不会深度代理。编辑器实例本身不需要响应式我们只是拿它来调用方法所以shallowRef是正解。// 错误做法 const editorRef ref() // 会深度代理可能出问题 // 正确做法 const editorRef shallowRef()这个点官方文档里有提但很不起眼容易被忽略。我见过有人因为用了ref编辑器正常能用但在某些操作后光标会跳到开头查了很久才定位到这里。3.3 v-model 双向绑定与 onChange 的取舍上面代码里我同时用了v-modelvalueHtml和onChange。这里其实有个小细节。Editor组件本身支持v-model它会自动同步编辑内容到绑定的变量。但我要把这个内容往外抛给父组件用的是emit。有人会问那我能不能只用v-model不写onChange可以但要注意时序。v-model更新的是valueHtml而emit需要在内容变化后触发。如果你只依赖v-model然后在外层用watch(valueHtml)去 emit也是可行的但多了一层 watch。我习惯直接在onChange里 emit逻辑更直白。不过有个坑要提醒onChange触发非常频繁。用户每敲一个字都会触发。如果你的父组件在收到 emit 后做了重操作比如实时调接口做保存、实时全文搜索页面会明显卡顿。这种场景我就用一个简单的防抖let timer null const handleChange () { clearTimeout(timer) timer setTimeout(() { emit(update:modelValue, valueHtml.value) }, 300) }提示如果只是本地保存到变量不涉及网络请求或重计算就不用防抖直接 emit 最省事。防抖只在父组件处理成本高的时候才加。3.4 销毁时机onBeforeUnmount 里必须 destroyonBeforeUnmount里那行editor.destroy()不能省。wangEditor 在初始化时会挂载各种 DOM 事件监听、创建内部对象。如果组件卸载时不做销毁这些引用会留在内存里造成内存泄漏。在后台管理系统里用户频繁切换页面泄漏累积起来会很明显——表现为页面越用越卡最后不得不刷新。这里还有个顺序问题destroy要在组件真正卸载前调用所以用onBeforeUnmount而不是onUnmounted。这个细节很小但用错了偶尔会出现销毁时找不到 DOM 节点的警告。3.5 父组件怎么用这个封装好的编辑器封装成组件后父组件调用就很清爽了template div classarticle-edit RichEditor v-modelarticleContent / el-button typeprimary clicksubmit保存/el-button /div /template script setup import { ref } from vue import RichEditor from /components/RichEditor.vue const articleContent ref(p初始内容/p) const submit async () { await saveArticle({ content: articleContent.value }) } /script这样编辑器就成了一个纯粹的受控组件父组件只管拿 HTML 字符串不用关心内部实现。这套封装我在好几个项目里复用基本没出过问题。4. 图片上传不接自己的后端等于白做富文本编辑器最容易出问题的模块就是图片上传。默认配置下wangEditor 的插入图片功能可能用的是临时方案你的文件根本不会传到自己的服务器。做中后台系统这块必须接自己的上传接口否则内容保存了图片却丢了。4.1 MENU_CONF 里配置 uploadImage图片上传的配置写在editorConfig.MENU_CONF里const editorConfig { placeholder: 请输入内容..., MENU_CONF: { uploadImage: { server: /api/upload/image, fieldName: file, maxFileSize: 5 * 1024 * 1024, allowedFileTypes: [image/*], headers: { Authorization: Bearer localStorage.getItem(token) }, customInsert(res, insertFn) { // 根据后端返回结构调整 insertFn(res.data.url, res.data.alt || , res.data.href || ) } } } }几个参数解释一下。server是你后端的上传接口地址fieldName要和后端接收的参数名一致默认是filemaxFileSize是单文件大小限制我一般设 5MB太大容易拖慢页面allowedFileTypes限制类型防止用户传奇怪的东西上来。customInsert是重点。默认情况下wangEditor 认为后端返回{ errno: 0, data: { url: ... } }这种结构。如果你的后端返回格式不一样大概率不一样就必须用customInsert自己处理把真正的图片地址提取出来调insertFn插进去。4.2 对接后端上传接口的两种常见返回格式国内后端常见的返回格式大致两种后端框架典型返回customInsert 处理自定义统一响应{ code: 200, data: { url } }取res.data.url若依风格{ code: 200, url: ... }取res.url直传 OSS直接返回 url 字符串直接用 rescustomInsert的写法要跟着返回格式走。这里的关键是你要清楚后端到底返回了什么。我通常会在customInsert里先打一行console.log(res)确认结构后再写取值逻辑别凭想象写。4.3 上传失败和超时的兜底处理默认上传失败时编辑器会弹一个提示。但这个提示信息有时候不够用——比如是 token 过期还是文件太大用户看不出来。我一般会补一个失败回调MENU_CONF: { uploadImage: { // ...前面的配置 onFailed(file, res) { console.error(上传失败, file, res) // 这里接你自己的提示组件 Message.error(res.message || 图片上传失败) }, onError(file, err, res) { console.error(上传异常, file, err, res) Message.error(图片上传异常请重试) } } }onFailed是后端返回失败比如业务逻辑拒绝onError是网络层异常比如超时、断网。这两个分开处理用户能拿到更准确的反馈。注意大图上传还有个隐性问题——服务器上传耗时较长时maxFileSize卡住的是文件大小但不卡耗时。如果接口很慢用户会以为编辑器卡死了。可以在上传前用onBeforeUpload做一次压缩或者直接提示用户。这部分我在后面性能那节再展开。5. 只读模式、内容回显与样式把控编辑器不只有编辑态。详情页、预览页要用只读模式展示内容这块处理不好会出现内容能显示但格式乱了工具栏还在但点不动之类的尴尬。5.1 用 mode 切换 edit 和 default别用 disabledwangEditor 的只读是通过mode属性控制的。Toolbar和Editor都要传同一个modeToolbar :editoreditorRef :modemode / Editor v-modelvalueHtml :modemode onCreatedhandleCreated /mode只有两个值default编辑和simple另一种编辑态工具栏更简化。如果你要真正的只读做法是隐藏 Toolbar然后给 Editor 传一个不可编辑的配置或者干脆不渲染 Toolbar。我常用的模式是给编辑器加一个readonly属性然后在组件内部判断Toolbar v-if!readonly :editoreditorRef :modemode / Editor v-modelvalueHtml :modemode :defaultConfigreadonly ? readonlyConfig : editorConfig onCreatedhandleCreated /其中readonlyConfig里把编辑功能关掉。更简单粗暴但有效的做法是只读时只渲染内容用 CSS 把编辑区设置成不可交互同时不渲染 Toolbar。这样用户看不到那一排按钮体验更干净。5.2 内容回显时最容易出现的问题内容回显就是把数据库里的 HTML 字符串塞回编辑器。看起来很简单实则有两个高频问题。第一个是回显时机。如果你在组件还没创建 editor 实例时就把内容赋给了valueHtml可能有概率丢失。稳妥的做法是等onCreated触发后再设置内容或者确保v-model绑定的初始值在组件挂载时就有。我一般用v-model绑定一个已经赋好值的变量配合onCreated里的实例基本不会有问题。第二个是HTML 被清洗。有些后端或者前端路由会对 HTML 做转义比如把变成lt;结果回显出来是一堆尖括号。这种情况要检查数据流看是哪一层做了转义。定位方法是先console.log一下拿到的字符串看它到底是 HTML 还是转义后的文本。5.3 全局样式污染与容器隔离富文本编辑器的内容样式和你的后台管理系统全局样式很容易打架。比如你的后台用了某个 UI 框架全局设置了p { margin: 0 }而编辑器内容里p标签本来该有间距结果就挤成一团。解决办法有两个方向。一是作用域隔离把编辑器容器限定在一个 class 下样式只作用于这个范围.editor-article { font-size: 15px; line-height: 1.8; } .editor-article p { margin-bottom: 12px; }二是只读展示区单独设置样式因为详情页往往不在编辑器组件里而是一个普通的v-html容器。这时要自己写好内容样式别指望全局样式能覆盖到。提示v-html渲染富文本内容时要确保内容来源可信比如自己后台录入的避免直接渲染用户可控的 HTML。这是基础的安全意识不是可选项。5.4 工具栏按需定制删掉那些用不上的按钮不是所有项目都需要全量工具栏。有些不必要的功能比如上传视频、插入地图留着反而困扰用户。wangEditor 支持通过toolbarConfig.excludeKeys排除按钮const toolbarConfig { excludeKeys: [ group-video, insertVideo, uploadVideo, fullScreen ] }排除之后工具栏更简洁加载也更快。如果反过来你想加某个默认没开的按钮用insertKeys补上。一般情况下默认工具栏已经够用按需裁剪即可。6. 打字卡顿、大内容编辑与性能调优编辑器在内容少的时候跑得飞起但一旦文档变长或者用户连续输入就可能出现明显卡顿。这一节讲我怎么定位和解决这些性能问题。6.1 定位卡顿先分清是编辑器还是页面遇到卡顿别急着怪编辑器。先用一个笨办法判断把这个编辑器放到一个空白页面里只留它自己输入同样多的内容。如果还是卡问题在编辑器配置或内容本身如果不卡了那就是你的页面里有别的东西在拖后腿。我遇到过一次典型情况页面卡顿查了半天发现是onChange里触发了父组件的一个 watch而那个 watch 又同步更新了一个大数据量的表格。编辑器本身没问题是数据联动导致的连锁反应。6.2 长文档编辑的应对策略wangEditor 是所见即所得编辑器内部维护 DOM。当文档特别长比如几万字DOM 节点数量上来后任何编辑器都会吃力。这不是 wangEditor 独有的问题。我的应对策略有这么几条限制单篇内容长度后台内容录入一般有实际长度上限超过就提示分段。从产品层面规避超长文档。减少 onChange 中的重操作前面提过的防抖在这里同样适用。图片懒加载长文档里图片多可以在只读展示时对图片做懒加载编辑态则保持原样。避免在编辑器外层套复杂的响应式容器外层容器越大、响应式依赖越多编辑器重渲染的代价越高。6.3 打包体积按需引入与首屏优化wangeditor/editor本身不算特别大但加上样式和处理逻辑还是会有一定体积。对首屏要求高的系统我会把编辑器组件做成异步组件const RichEditor defineAsyncComponent(() import(/components/RichEditor.vue))这样编辑器只在真正需要它的页面才加载不拖累登录页、首页这些高频首屏。这个优化成本很低收益却很明显尤其对中小型打包体积敏感的项目。6.4 一个容易忽略的兼容性问题编辑器依赖MutationObserver、Selection等 DOM API这些在现代浏览器都没问题。但如果你要兼容较老的浏览器需要提前确认。我在实际项目里遇到过一个边界情况某些环境下光标的 Selection 行为有差异导致光标定位不准。遇到这种问题先确认浏览器版本再考虑是不是要反馈到编辑器社区。另外Vite 项目里如果用了某些自定义的构建配置注意确认样式文件被正确包含。小概率会出现开发环境正常、生产环境样式丢失的情况这种一般是构建配置问题不是编辑器的问题。7. 封装成可复用组件后的维护经验单个页面跑通只是开始。真正决定体验的是把这个编辑器封装成一个团队里能复用的组件之后怎么维护它。7.1 把上传逻辑抽出去别写死在组件里我见过不少项目uploadImage 的配置直接写死在编辑器组件内部。结果换个项目、换个上传接口就得改组件源码。正确的做法是把上传配置作为 props 传进来或者用一个独立的useEditorUpload组合式函数管理。// useEditorUpload.js export function useEditorUpload(api) { return { uploadImage: { server: api.uploadUrl, fieldName: file, headers: { Authorization: api.getToken() }, customInsert(res, insertFn) { insertFn(res.data.url) } } } }这样编辑器组件只负责编辑上传策略由使用方决定。职责分离之后组件复用起来就轻松多了。7.2 props 设计要留出扩展空间封装组件的时候别只留一个modelValue。至少还应该支持readonly只读态。height编辑区高度不同页面需求不同。placeholder占位提示文案。toolbarConfig/editorConfig允许使用方覆盖默认配置。const props defineProps({ modelValue: { type: String, default: }, readonly: { type: Boolean, default: false }, height: { type: String, default: 400px }, placeholder: { type: String, default: 请输入内容... }, toolbarConfig: { type: Object, default: () ({}) }, editorConfig: { type: Object, default: () ({}) } })留这些口子是为了避免以后每来一个新需求就要改组件。组件接口稳定维护成本才低。7.3 版本升级的风险控制wangEditor 的版本升级特别是从 4.x 到 5.x 这种大版本API 变化很大。我的习惯是锁版本在package.json里用确定版本号不用^或~避免一次npm install之后编辑器行为悄悄变了。wangeditor/editor: 5.1.23, wangeditor/editor-for-vue: 5.1.12如果确实要升级先在分支上升跑一遍所有用到编辑器的页面重点测内容回显、图片上传、只读展示、样式是否错乱。大版本升级绝不轻率合入主分支。7.4 几个我在实际项目里总结的小技巧最后说几个不写在文档里、但实战中很有用的技巧。初始化内容为空时给个默认段落。有些设计稿希望编辑器一打开就有一行占位可以在初始化时设置valueHtml为pbr/p这样光标能正常落进去。空字符串有时候会让编辑区看起来点不动。处理粘贴内容。用户从 Word 里粘贴常带有大量内联样式和垃圾标签。wangEditor 有一定的清洗能力但如果你对内容洁净度要求高可以在onChange后对 HTML 做一次服务端或前端的过滤。这个要谨慎别把合法格式也洗掉了。图片插入后的对齐。默认插入的图片是块级独立一行如果你的内容需要图片文字混排得在编辑器配置或样式里额外处理。简单做法是给小图设置display: inline-block但这会影响整体排版建议按项目需要单独调。cursor 和 focus 的控制。有些场景需要在打开编辑器后自动聚焦或者插入内容后把光标定位到末尾。这些可以借助 editor 实例的方法来实现但要注意调用时机必须在实例创建之后。内容长度和大小的存储考量。HTML 字符串存数据库字段类型别用太小。一段带图片的富文本光 URL 就不少字符。用 text 或 mediumtext 更稳妥别到时候内容被截断。把这些都踩过一遍之后现在我在 Vue3 项目里集成 wangEditor基本能做到一次配置、稳定复用。编辑器的坑不在于功能多复杂而在于那些文档里一笔带过、实际却高频出现的细节。搞清shallowRef的原因、接好自己的上传接口、做好只读和销毁剩下的就都是常规开发了。
返回列表