富文本编辑器几乎是内容后台和管理系统的标配模块,但 Vue 3 项目在集成 Quill 与 wangEditor 时,如果只是照着文档快速挂载,后续遇到图片上传、自定义按钮、内容校验等需求往往会变得被动。本文从两者的架构定位出发,分别给出在组合式 API 下的完整配置思路,并重点讨论二次开发中容易忽略的响应式包装、生命周期销毁和样式冲突问题。

一、选型前先看架构:Quill 与 wangEditor 的定位差异
Quill 的核心是文档模型,基于 Delta 格式描述内容变更,它的优势是数据层稳定、可预测,适合需要协同编辑、版本记录或者自定义块级格式的项目。Quill 的扩展主要通过 parchment 和 module 完成,但中文文档相对薄弱,图片上传、视频插入等常见操作需要自己封装 handler。
wangEditor 则把中文后台场景的常用功能做成了默认能力,包括图片上传、视频、表格、附件、代码块等。它的菜单系统基于插件注册,二次开发时通常只需要实现菜单的工厂函数和渲染逻辑。缺点是内部数据模型不如 Quill 清晰,复杂格式的自定义成本更高。
从集成成本看,两者也有明显区别。Quill 官方没有提供 Vue 3 专用组件,需要手动创建实例并手动销毁,对初学者有一定门槛;wangEditor 提供了 @wangeditor/editor-for-vue 包,天然支持 Vue 3 的组件式用法,工具栏和编辑区都是现成组件。正因如此,wangEditor 在中小型 Vue 3 项目中的集成速度明显更快。但快不意味着没有约束,wangEditor 的编辑器实例必须放在 shallowRef 中,如果直接丢进 ref 或 reactive,深层响应式代理会带来渲染性能问题,甚至导致编辑器初始化异常。Quill 虽然不存在这个包层问题,但手动挂载时需要谨慎处理 DOM 容器的获取时机,避免在 onMounted 之前访问容器。
二、Quill 在 Vue 3 中的配置与二次开发
Quill 的风格配置通常包含主题和工具栏两部分。snow 主题自带浮动工具栏和边框,bubble 主题适合轻量评论场景。下面是一个 Vue 3 组件的基础挂载示例,容器通过 ref 绑定,编辑器实例用普通变量保存,避免被 Vue 代理:
npm install quill
import { onMounted, onBeforeUnmount, ref } from 'vue'
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
const editorBox = ref(null)
let quillInstance = null
onMounted(() => {
quillInstance = new Quill(editorBox.value, {
theme: 'snow',
placeholder: '请输入正文...',
modules: {
toolbar: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image', 'code-block']
]
}
})
})
onBeforeUnmount(() => {
quillInstance = null
})
这个示例中的 toolbar 配置覆盖了加粗、斜体、下划线、标题、列表、链接、图片和代码块。实际项目里可以根据角色权限动态裁剪工具栏,比如普通用户只保留基础格式,运营人员才开放代码块和上传入口。Quill 的模块系统允许在初始化后通过 getModule 获取 toolbar 实例,再动态调整 handler,灵活性较高。
图片上传是 Quill 二次开发最常见的环节。默认情况下,点击图片按钮会插入 base64 或原始 URL,生产环境通常需要上传到对象存储。通过 toolbar 模块的 addHandler 可以替换默认行为:
const toolbar = quillInstance.getModule('toolbar')
toolbar.addHandler('image', () => {
const input = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('accept', 'image/*')
input.click()
input.onchange = async () => {
const file = input.files[0]
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', { method: 'POST', body: formData })
const data = await res.json()
const range = quillInstance.getSelection(true)
quillInstance.insertEmbed(range.index, 'image', data.url)
}
})
这里使用原生 input 元素触发文件选择,再用 FormData 上传。上传成功后通过 insertEmbed 在光标位置插入图片,避免替换整个编辑器内容。需要注意 addHandler 会完全覆盖内置图片处理逻辑,如果需要在插入前压缩、校验尺寸或限制文件类型,可以在 onchange 中加入对应判断。另一个常见做法是自定义 toolbar 配置中的 image 为 false,然后单独渲染一个按钮来绑定上传函数,这样视觉上更可控。
除了上传,Quill 的 parchment 还允许注册自定义格式。比如需要给文本加一个黄色高亮标记,又不想依赖现有的 background 格式,可以扩展 Attributor:
import Quill from 'quill'
const Parchment = Quill.import('parchment')
class MarkStyle extends Parchment.Attributor.Style {
value(node) {
return node.style.backgroundColor === 'yellow' ? 'mark' : ''
}
}
Quill.register(new MarkStyle('mark', 'background-color', { scope: Parchment.Scope.INLINE }), true)
注册完成后,就可以在 toolbar 中加入 { 'mark': 'mark' },让用户一键切换高亮状态。这种扩展方式比直接操作 DOM 可靠,因为 Quill 会用 Delta 记录格式变化,复制、撤销、重做都能保持一致。
三、wangEditor 在 Vue 3 中的配置与二次开发
wangEditor v5 的 Vue 3 支持由官方包提供,安装依赖如下:
npm install @wangeditor/editor @wangeditor/editor-for-vue
它的组件化程度很高,工具栏和编辑区分别用 Toolbar 和 Editor 组件渲染。一个完整的模板结构如下:
<template>
<div style="border: 1px solid #ccc; z-index: 100;">
<Toolbar
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="default"
/>
<Editor
:modelValue="valueHtml"
@onChange="handleChange"
:defaultConfig="editorConfig"
mode="default"
/>
</div>
</template>
对应的 script 需要引入样式,并用 shallowRef 保存编辑器实例。注意 valueHtml 的初始值可以是 HTML 字符串,但必须等编辑器创建完成后再回填,否则内容不会显示:
import '@wangeditor/editor/dist/css/style.css'
import { onBeforeUnmount, ref, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
const editorRef = shallowRef()
const valueHtml = ref('<p>hello</p>')
const toolbarConfig = {
excludeKeys: ['group-video', 'fullScreen']
}
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
server: '/api/upload',
fieldName: 'file',
maxFileSize: 10 * 1024 * 1024
}
}
}
const handleChange = (editor) => {
valueHtml.value = editor.getHtml()
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
wangEditor 的 MENU_CONF 集中管理菜单级配置,uploadImage 可以指定服务端接口和字段名,也可以改用 customUpload 实现带鉴权头的上传逻辑。文件大小限制、图片宽度高度校验等也都在这里完成。如果后端返回的数据结构不是常见的 errno 和 data 格式,需要在 customUpload 中处理响应并调用 insertFn 插入图片地址。
二次开发方面,wangEditor 的菜单通过 factory 生成,自定义按钮需要提供标题、图标和回调逻辑。下面的示例演示了一个插入变量的菜单,用户点击后会在光标处写入带有花括号的占位文本:
const insertVariableMenu = {
key: 'insertVariable',
factory() {
return {
title: '插入变量',
iconSvg: '...',
menu: [
{ key: 'customerName', text: '客户姓名' },
{ key: 'contractNo', text: '合同编号' }
],
onSelect(key, editor) {
editor.insertText(`{{${key}}}`)
}
}
}
}
这个自定义菜单需要合并到 toolbarConfig 的 toolbarKeys 中,并保证 key 不与内置菜单冲突。iconSvg 可以直接使用 SVG 字符串,从设计系统或图标库中导出。与 Quill 相比,wangEditor 的自定义菜单更偏向 UI 层扩展,数据层的自定义格式支持较少,因此在需要复杂块级结构或严格内容校验时,Quill 反而更容易把控。
四、避坑与维护:响应式代理、销毁时机与样式隔离
Vue 3 的响应式系统对编辑器实例并不友好。Quill 实例如果被 ref 包裹,虽然 Vue 不会深度代理类实例,但若放入 reactive 对象中,代理会干扰内部事件绑定和属性读取。wangEditor 官方明确要求使用 shallowRef,因为其 Editor 实例含有大量内部状态,深层代理会导致编辑卡顿或功能异常。正确做法是:能用普通变量就用普通变量,需要暴露给模板时用 shallowRef,而不是 ref 或 reactive。
销毁时机同样容易忽略。SPA 中页面切换时,如果编辑器未销毁,会残留全局事件监听、定时器或上传队列,造成内存泄漏。Quill 需要手动将实例置空并移除 DOM 引用,wangEditor 需要调用 destroy 方法。建议统一在 onBeforeUnmount 中处理,如果组件被 keep-alive 缓存,还要结合 onActivated 和 onDeactivated 做暂停与恢复,否则返回页面时工具栏可能失效。
样式隔离和层级问题是实际联调中的高频故障点。Quill 的 snow 主题和 wangEditor 的样式文件都可能与项目里的重置样式冲突。wangEditor 的工具栏和下拉菜单使用绝对定位,如果父容器设置了 overflow: hidden 或较低 z-index,会出现菜单被截断的问题。解决办法是保证编辑器容器 z-index 足够高,并避免在编辑器外层使用 overflow: hidden。另外,两库都需要在入口引入对应 CSS,漏掉会导致编辑器布局错乱,尤其在按需加载样式的构建配置下更容易出现。
内容回显和受控模式也需要额外注意。两个编辑器都支持通过 getHtml 获取内容,但回显时机要在编辑器创建完成后,否则内容为空。wangEditor 的 modelValue 是单向数据流,onChange 回调中手动更新 valueHtml,不能直接修改 prop。Quill 则需要用 setContents 或 clipboard.dangerouslyPasteHTML 恢复内容,前者适合保存 Delta 的场景,后者适合恢复已存在的 HTML。
综合来看,Quill 与 wangEditor 并不是简单的好坏之分,而是不同架构偏好的体现。选择 Quill 意味着接受稍高的封装成本,换取更清晰的数据层和更细粒度的格式控制;选择 wangEditor 则能快速获得中文后台所需的大部分功能,并在后续通过插件机制做轻量扩展。无论选择哪一个,在 Vue 3 中正确处理实例生命周期、响应式边界和样式隔离,都是保证编辑器稳定运行的前提。
Vue 3 富文本编辑器Quill 配置wangEditor 二次开发修改时间:2026-09-22 11:15:58