ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Quill富文本编辑器配置实战:从Delta到工具栏的完整指南

2026/9/8 16:38:57 拓冰建站 浏览量
Quill富文本编辑器配置实战:从Delta到工具栏的完整指南 搞前端这么多年接触过的编辑器不在少数从早期的UEditor、wangEditor到后来的TinyMCE、CKEditor再到现在主流的Quill每个都有自己的脾气。之前做后台管理系统的时候富文本编辑、Markdown编辑、内容发布这类需求几乎是标配而Quill是我用过之后感觉最舒服、也最愿意一直保留下来的编辑器。这篇文章就从配置的角度把我实际项目中积累的细节、踩过的坑、以及一些不常在文档里看到的小经验写出来希望能帮你少走弯路。1. 为什么选Quill做富文本编辑器——选型逻辑1.1 富文本编辑器到底在解决什么问题稍微正经一点的网站、管理后台、小程序运营平台基本都绕不开富文本编辑。用户要在网页上像用Word一样写文章、插图片、调格式然后把这段富文本内容存起来、再排版展示出来。这时候富文本编辑器就充当了一个“网页版排版工具”。很多人一上来就想着随便套一个编辑器插件就完事但实际动手才发现粘贴过来样式乱掉、图片传上去是个base64长串、加张表不知道怎么扩展、前端改个颜色后端解析不到。这些问题不是编辑器本身不好用而是你没有把它当成一个核心模块去配置。Quill的强项恰恰就在这里——结构清晰、扩展性强、文档数据干净。1.2 主流的几个编辑器横向对比为了不让你觉得我是在硬吹Quill我把这几年接触过的几款编辑器按实际使用体验做个对比。编辑器上手难度扩展性数据格式典型坑点UEditor低差纯HTML依赖jQuery样式侵入强接口老旧wangEditor低中HTML轻量但复杂定制比较费劲TinyMCE中强HTML功能全但包体积大配置项多到眼花CKEditor 5中强XML/HTML新版模块化复杂坑也多Quill中极强Delta JSON上手需要先理解Delta概念Quill有几个典型的优点一是它不依赖jQuery原生框架也能轻松配合Vue、React二是它通过Delta这种JSON格式来保存内容数据结构非常清晰不受HTML标签混乱影响三是它默认UI干净整洁视觉上很舒服。对前后端分离的项目尤其友好。1.3 Quill的几个关键设计理念Quill有三大设计理念以Delta为数据核心、模块化架构、主题可定制。这三个理念决定了你要不要用Quill也决定了你会怎么配置它。Delta是最基础的它是一种JSON格式的富文本描述。用Quill保存内容的时候你拿到的不是一段HTML而是一串描述文本操作的JSON数据。比如[{ insert: Hello }, { insert: World, bold: true }]这段数据表示“Hello ”和加粗的“World”。因为结构规范、语义清晰存储、传输、回显都有天然优势。你也不用担心用户在编辑器里可以随便贴各种乱七八糟的HTML标签导致后端没法处理。模块化架构就更好理解了。Quill给的工具栏、文字颜色、行高、上传图片等能力本身都是一个个模块你可以按需取舍也可以自己写新模块加进去。很多人在配置Quill的时候不敢乱动工具栏其实Quill的灵活性就在于你可以决定哪些功能出现、哪些不要。主题机制让Quill默认的样式就非常清爽你很难看到一个开着Quill的页面乱糟糟的。总之选Quill这件事它的设计初衷就是“给开发者一套干净、可扩展的基础设施”而不是把所有功能都堆在一起。如果你想要一个“开箱即用、背后还能保持灵活”的编辑器Quill是个很好的选择。2. Quill配置前必须搞懂的核心概念2.1 不要被官方文档吓到Delta到底是个什么东西Quill官网上Delta的内容看起来理论性很强很多前端同学第一次看会头皮发麻又是insert又是retain又是delete跟操作DOM完全不是一个思路。我换个生活化的类比来解释如果把富文本编辑看作搭积木每块积木都有自己的颜色、大小、位置Delta就是这个搭积木过程的“说明书”每一条指令都记录“在某个位置插入什么”“保持住哪些格式”“删掉哪一段”。举个例子一段文字内容是这样的[ { insert: 这是 }, { insert: 标题, header: 1 }, { insert: \n } ]翻译过来就是插入“这是”然后插入“标题”并且把它的样式级别设为一级标题最后换行。[]是整个Delta文档的数组。看到这个结构你就能明白为什么Quill保存的内容不怕样式丢、不怕标签冗余——因为它本身就是结构化数据。实际项目中我们通常不会直接手写Delta而是用quill.getContents()获得Delta数据、用quill.setText()或quill.setContents()恢复内容。但要排查问题懂一点Delta很有帮助。2.2 工具栏Toolbar是怎么工作的Quill的工具栏不是一堆写死按钮而是通过配置项驱动的。你给它一个数组它按这个数组渲染对应控件。比如最简单的方式是在HTML里放一个div#toolbar给它一串classql-bold的按钮Quill初始化时会自动识别。不过我更推荐在JS的modules.toolbar里直接配置容器这样一目了然const quill new Quill(#editor, { modules: { toolbar: [ [bold, italic, underline, strike], [{ header: [1, 2, 3, false] }], [{ list: ordered }, { list: bullet }], [blockquote, code-block], [link, image] ] } });注意工具栏每行的分组是嵌套数组同一组内按钮并排显示不同组之间会有分隔条。这一格式很容易被忽略写错了某组按钮可能就不显示。2.3 主题与初始化参数Quill初始化最简单的形式是const quill new Quill(#editor, { theme: snow, // 或 bubble modules: { toolbar: true }, placeholder: 请输入正文... });theme: snow是目前绝大多数项目用的主题白底、带工具栏边框干净。theme: bubble则是无边框、点击后浮出工具栏看起来更轻量但一般后台会用snow而不是bubble。需要提一句的是Quill 2.0以后在引入样式时跟旧版本略有不同。旧版本写法是node_modules/quill/dist/quill.snow.css就能搞定新版需要注意包的导出路径。我建议先去官网文档的Quick Start确认当前版本的CDN或NPM引入方式否则样式错乱会让人排查很久。3. Vue3项目里Quill的完整配置实操3.1 安装引入与基础初始化现在很多业务是Vue3工程我先说在Vue3里搞定Quill配置的全流程。最直接的思路是使用vueup/vue-quill它专为Vue3封装了组件用起来很省事。安装npm install vueup/vue-quill全局注册或按需引入都行。我习惯按需引入干净一些template QuillEditor v-model:contentcontent contentTypehtml themesnow toolbarfull / /template script setup import { QuillEditor } from vueup/vue-quill import { ref } from vue const content ref(p初始内容/p) /script这里有几个地方容易出问题。第一v-model:content才是绑定编辑内容的正确姿势直接v-model绑定的是Quill内部的Delta对象很多人第一次用会搞混。第二contentType可以选html还是delta如果你后端存储接口要HTML就选html如果你希望数据更结构化就选delta。如果你不想用这个封装而是直接用原生Quill也完全可以template div div reftoolbarRef/div div refeditorRef/div /div /template script setup import { onMounted, ref } from vue import Quill from quill import quill/dist/quill.snow.css const editorRef ref() const toolbarRef ref() let quill onMounted(() { quill new Quill(editorRef.value, { theme: snow, modules: { toolbar: toolbarRef.value } }) }) /script这种方式更接近Quill本身的使用方式方便定制也方便集成到任何框架。vueup/vue-quill这个封装的底层也是这套逻辑只是把常用配置包了一层。3.2 自定义工具栏配置详解先说一个小原则能用自定义工具栏解决的不要硬编码一堆HTML。因为你会发现Quill的工具栏跟后端解析、样式加载都有关结构写乱了后面维护成本很高。我在实际项目中是这样配的const toolbarOptions [ [bold, italic, underline, strike], // 基础行内样式 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题级别 [{ color: [] }, { background: [] }], // 文字颜色、背景色 [{ font: [] }], // 字体 [{ align: [] }], // 对齐方式 [blockquote, code-block], // 引用和代码块 [{ list: ordered }, { list: bullet }], // 有序/无序列表 [{ indent: -1 }, { indent: 1 }], // 缩进 [link, image], // 插入链接和图片 [clean] // 清除格式 ]注意color和background所在的小组里后面接一个空数组[]表示Quill会渲染出一个完整的色板。如果你写成[{color: #333333}]那就只有一种颜色容易掉坑里。另外font选项默认只有“Sans Serif”“Serif”“Monospace”几个系统字体如果要动态加载中文字体需要单独做扩展。配好工具栏后在初始化时的modules里传modules: { toolbar: toolbarOptions }如果你希望某个按钮绑定自定义行为比如点击“导出PDF”把当前编辑器内容转成文件那就要在Toolbar配置里占一个空位并且监听事件后面专门讲。3.3 常用模块配置说明Quill的模块化设计让它的主包非常干净但同时意味着一些常用功能都需要通过模块配置来开启。最核心的模块有这几个Toolbar模块控制工具栏布局刚刚已经说了。Keyboard模块控制键盘绑定比如回车、Tab、快捷键等大多数项目不用改。Clipboard模块控制粘贴行为对“粘贴过来的样式乱七八糟”这个问题非常关键。History模块控制撤销/重做的历史记录栈。以Clipboard为例如果不做处理用户从Word或者网页复制一大段内容到编辑器里Quill会自动尝试把多余样式清洗掉但有时还是会留下不少行内样式。我们可以配置粘贴规则比如统一把span style...过滤成纯文本clipboard: { matchVisual: false, matchers: [ [span, (node, delta) { return delta }] ] }更重要的是在text-change事件里对粘贴内容做统一清洗这个后面在事件配置里再说。再比如历史记录Quill默认能记录的深度是100次操作如果你的用户写长文经常撤销过头可以调大history: { maxStack: 500, userOnly: true }至于其他模块像“字数统计”“图片缩放”就需要自己扩展了Quill官方没有内置这些买了“开箱即用”账的人往往会在这里卡一下。3.4 事件监听与内容交互Quill提供两类核心事件text-change和selection-change。在Vue项目里很多人会直接在模板上text-change但更稳妥的方式是拿到Quill实例后手动监听因为有时候组件内部事件与Quill事件混在一起语义会不清晰。text-change能告诉你编辑器内容什么时候发生变化比如实时统计字数quill.on(text-change, () { const text quill.getText().trim() const length text.length console.log(当前有效字符数, length) })注意这里用getText()拿到的是纯文本如果直接用getContents()或者container.innerHTML统计会把样式标记、空段落标签都算进去。selection-change则可以监听光标位置变化适合做“输入框聚焦”、“展示当前格式”之类的功能quill.on(selection-change, (range, oldRange, source) { if (range) { console.log(用户选中了, range.index, 到, range.index range.length) } })实际项目中还有一个很实用的场景当用户编辑完一篇文章点“保存”按钮时我们需要先把编辑器内容清洗一遍再做后续提交。你可以在保存函数里这样取内容const html quill.root.innerHTML但这样拿到的是带很多ql-align-center这类类名的标准HTML如果你后端已经有现成的HTML解析器问题不大如果后端希望存纯文本摘要我一般会再取一次纯文本给后台const plainText quill.getText().trim().slice(0, 100)3.5 配置图片上传图片上传是富文本编辑器里最典型的坑位默认情况下Quill会把图片转成base64塞进编辑器里试几百字的文章没事真要写个图文混排的长文就会出现内容巨长、请求太大、数据库存储吃紧、回显变慢一堆连锁问题。正确的思路是把图片传给自己的后端或云存储然后在编辑器里插入图片URL。实现这一步通常靠扩展Quill的工具栏里image按钮行为。一种常见方式是在Toolbar配置里用自定义按钮并监听它去触发文件选择器上传const imageHandler () { 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 response await fetch(/api/upload, { method: POST, body: formData }) const data await response.json() const url data.url const range quill.getSelection(true) quill.insertEmbed(range.index, image, url) quill.setSelection(range.index 1) } } const toolbarOptions [ ... [{ image: imageHandler }] // 注意不是字符串image而是自定义函数 ]在Vue3封装组件时加自定义按钮的思路类似你需要先在Quill上注册一个模块然后把按钮行为暴露出去。总而言之图片上传的核心就是“拦截默认行为替换成异步上传URL再插入”这个思路不受具体后端语言限制非常通用。4. 常见问题与排查技巧实录4.1 图片粘贴成base64导致内容爆满这个问题太典型了几乎每个项目都要遇到。用户在本地截图后直接CtrlV粘贴Quill默认会读取图片转成base64插进编辑器。一篇长文下来一段base64串能占到几十KB甚至几MB后端存也不是响应也慢。解决思路不要强行在配置里禁止粘贴图片而是拦截粘贴事件把剪贴板里的图片文件拦截下来、走统一上传接口再把上传返回的URL插入编辑器。具体实现是监听clipboard模块的paste行为或者在容器上加paste事件监听。还有一个辅助措施对已有内容做“体检”用quill.getContents()检查所有image节点如果发现src以data:image开头就提示用户重新插入或自动替换。不过在项目上线前就把粘贴上传逻辑做好更省心。4.2 服务端渲染和Nuxt场景下的坑如果你是用Nuxt或者其他SSR框架Quill这种依赖windowSDK的浏览器库在服务端会报错因为Node环境里没有document。遇到这种情况正确的做法是把Quill相关组件设置为只在客户端渲染在Vue组件外层套一层ClientOnly标签或者使用动态导入并设置ssr: false或者直接在created/mounted生命周期再做初始化。这不只是掉不掉链子的问题是在SSR框架里使用所有浏览器插件的通用常识。很多人一开始没注意直接全局注册结果服务端渲染直接崩掉。4.3 官方小细节默认字体不全与中文字体扩展Quill默认的字体只有很少几个如果你用惯了中文后台会觉得很别扭。中文项目一般希望有宋体、黑体、微软雅黑之类的字体选择。要实现这一点得先注册字体列表再往工具栏的font下拉框里加选项。Quill的做法是通过quill.register来注册一个新的Font类const Font Quill.import(attributors/class/font) Font.whitelist [sans-serif, SimSun, SimHei, Microsoft-YaHei, KaiTi] Quill.register(Font, true)然后工具栏里配置[{ font: [, sans-serif, SimSun, SimHei, Microsoft-YaHei, KaiTi] }]此外也要给对应的类名写CSS.ql-font-SimSun { font-family: SimSun, serif; } .ql-font-SimHei { font-family: SimHei, sans-serif; } .ql-font-Microsoft-YaHei { font-family: Microsoft YaHei, sans-serif; }这样下拉框里才有中文选项不然你光加白名单是无效的。细节虽小但用户体验提升非常明显。4.4 回显时内容样式不稳定保存HTML到后端回显时样式丢失或错乱也是高频问题。Quill生成的HTML里面会带ql-align-center、ql-size-large这类类名这些类名依赖Quill的CSS。如果你回显内容的前端页面没有引入Quill的样式文件那么对齐、颜色、大小就好坏参半。解决办法有两个回显页面也引入Quill的snow主题样式哪怕是简版回显前在内容外面套一个带ql-editor类的容器。我实际项目里用的是第一种在静态详情页直接引入link relstylesheet hrefhttps://cdn.quilljs.com/2.0.0/quill.snow.css /然后把内容丢进div classql-editor容器中。这种方式成本最低。4.5 体验优化字数统计、全屏编辑后台编辑器带了字数统计和全屏编辑观感会专业很多。字数统计在上面说过了监听text-change后触发更新。全屏编辑则可以借助Fullscreen模块或者自己监听按钮事件让编辑容器position: fixed铺满全屏同时给Quill的编辑区高度做自适应。这个只要CSS控制好不复杂但非常提升体验。5. 几个进阶自定义的小玩法5.1 自定义一个“清除格式且保留段落”的按钮Quill自带clean按钮作用是去掉选中内容的格式。但有时候我们希望“去掉格式但别把段落标签也拆了”这时候默认clean往往出力过猛把段落结构也破坏掉。一个稳妥的做法是自己写一个模块遍历选中区域的Delta只保留插入的文本和换行符不保留任何属性const customClean { init() {} } quill.getModule(toolbar).addHandler(customClean, function() { const range quill.getSelection(true) if (range) { const delta quill.getContents(range.index, range.length) const cleanText delta.ops .map(op op.insert || ) .join() quill.deleteText(range.index, range.length) quill.insertText(range.index, cleanText) } })把customClean注册进工具栏替换默认的clean体验会好很多。5.2 把Quill封装成自己的组件在大型后台系统里很多页面都会用到编辑器如果每个页面都复制一份Quill初始化代码后续改动会让人想骂人。我习惯把编辑器封装成公司内部统一的MyEditor.vue组件内部包含统一的上传API地址统一的工具栏配置统一的字数限制策略统一的样式主题暴露update:content事件这样后续组件要升级、修复、换皮肤只需要改一个地方全站生效。这对中型项目来说收益极大。5.3 与后端协同建议直接存Delta或清洗后的HTML很多团队后端存内容喜欢直接存富文本HTML但前端每次都要清洗很费劲。如果前后端都是你说了算我会强烈建议后端存储使用Delta JSON同时在数据库里冗余一个纯文本字段方便做搜索摘要。这样做的优势是前端回显时用Delta做精确渲染不会丢失任何格式信息后端做关键词搜索时用纯文本字段也不会被HTML标签干扰。这是我在项目里踩过不少坑之后的真实心得推荐指数五颗星。写在最后的一些体会每次接触一个新技术我都会先认真读它的核心概念再上手配置。Quill最让人舒服的就是它的设计思路足够“正”Delta数据模型、模块化机制、主题定制这三板斧能让很多复杂场景变得清晰。但反过来如果你跳过概念直接上手很多坑你能踩得一个不落。图片上传要改、字体中文要扩展、粘贴样式要清洗、回显CSS要提前准备这些经验不在官方文档的快速开始里却恰恰是决定一个编辑器好不好用的关键。希望这篇配置攻略能帮你在项目里少折腾几晚如果有更好用的配置思路也欢迎交流。