引言
在AIGC浪潮席卷各行各业的今天,为应用注入AI能力已从“锦上添花”变为“核心竞争力”。打造一个智能写作助手,深度融合AI与富文本编辑器,无疑是抢占下一代内容创作高地的关键一步。
而一切智能编辑的基石,在于一个稳定、强大且高度可定制的基础编辑器。本文将深度解析 Quill 2.x——这个在现代Web开发中备受青睐的富文本编辑器解决方案。快来开始Quill2.x的教程吧!
本文将从概念解析到实战落地,补充核心原理、汉化方案和避坑指南,帮你真正吃透 Quill 2.x,看完就能直接应用到项目中。
一、Quill 核心概念:它到底是什么?
在动手之前,先搞懂 Quill 的核心定位,避免用错场景:
Quill 是一款「API 驱动的富文本编辑器」,核心设计理念是「让开发者能精准控制编辑行为」。它不同于传统编辑器(如 TinyMCE、CKEditor)的「配置式黑盒」,而是通过暴露清晰的 API 和内部状态,让开发者像操作 DOM 一样操作编辑器内容。
几个关键概念需要明确:
-
容器(Container) :用于承载编辑器的DOM元素,Quill会接管该元素并渲染编辑区域
-
模块(Modules) :编辑器的功能单元(如工具栏、代码块),2.x 中模块需显式注册。
-
主题(Themes) :编辑器外观,官方提供
snow(带固定工具栏)和 bubble(悬浮工具栏)两种,支持自定义样式。
-
Delta:Quill 独创的内容描述格式(类似 JSON),用于表示内容本身和内容变化,是实现协同编辑、版本控制的核心。
-
格式(Formats) :描述内容的样式属性(如加粗、颜色、链接),可通过 API 或工具栏触发,支持自定义扩展。
二、原理解析:Quill 是如何工作的?
理解底层原理,能帮你更灵活地解决问题。Quill 的核心工作流程可分为三部分:
1. 内容表示:Delta 格式
传统编辑器用 HTML 字符串描述内容,但 HTML 存在「同内容多表示」(如 <b> 和 <strong> 都表示加粗)、「难以 diff 对比」等问题。而 Delta 用极简的结构解决了这些问题:
Delta 本质是一个包含 ops 数组的对象,每个 op 由 insert(内容)和 attributes(样式)组成。例如:
// 表示「Hello 加粗文本」的 Delta
{
ops: [
{ insert: '这是一段 ' },
{ insert: '加粗文本', attributes: { bold: true } }
]
}

- 优势 1:唯一性 —— 同一内容只有一种 Delta 表示,避免歧义。
- 优势 2:可合并 —— 两个 Delta 可通过算法合并(如用户 A 和用户 B 同时编辑的内容),是协同编辑的基础。
- 优势 3:轻量性 —— 比 HTML 更简洁,传输和存储成本更低。
2. 渲染机制:2.x 版本的性能飞跃
Quill 1.x 直接操作 DOM 渲染内容,当内容量大时容易卡顿。2.x 重构了渲染逻辑,采用「虚拟 DOM 思想」优化:
- 内部维护一份「文档模型(Document Model)」,作为内容的单一数据源。
- 当内容变化,先更新文档模型,再通过「差异计算」只更新需要变化的 DOM 节点。
- 减少 30% 以上的 DOM 操作,大幅提升大数据量场景(如万字长文)的流畅度。
3. 模块架构:功能的解耦与扩展
Quill 的所有功能都通过「模块」实现,核心模块包括:
-
toolbar:工具栏,控制格式按钮的显示和交互。
-
history:记录操作历史,支持撤销 / 重做。
-
table:2.x 原生支持的表格模块(1.x 需第三方扩展)。
-
clipboard:处理复制粘贴,自动过滤危险内容。
模块之间相互独立,开发者可按需注册,也能通过 Quill.register() 自定义模块,实现功能的灵活扩展。
三、快速入门:5 分钟搭建基础编辑器
安装依赖 -> 基础初始化 -> 核心API -> 预告
1. 安装依赖
bash
运行
# 核心包(2.x 版本)
pnpm add quill@2.x
# 表格模块(2.x 需单独安装,原生支持)
pnpm add @quilljs/table
2. 基础初始化
Step 1:HTML 容器
<div id="editor" style="height: 300px;"></div>
Step 2:引入并注册模块
import Quill from 'quill';
import 'quill/dist/quill.snow.css'; // 引入 snow 主题样式
import TableModule from '@quilljs/table'; // 表格模块
// 显式注册模块
Quill.register('modules/table', TableModule);
Step 3:初始化配置 - 方案一
const quill = new Quill('#editor', {
theme: 'snow', // 选择主题
modules: {
toolbar: {
container: [
// 每个数组是一个分组,里边每个项是一个工具栏最小配置单元
['bold', 'italic', 'underline', 'strike'], // 基本格式
['blockquote', 'code-block'], // 块引用和代码块
[{ 'header': 1 }, { 'header': 2 }], // 标题级别
[{ 'list': 'ordered'}, { 'list': 'bullet' }], // 有序列表和无序列表
[{ 'script': 'sub'}, { 'script': 'super' }], // 上标和下标
[{ 'indent': '-1'}, { 'indent': '+1' }], // 缩进
[{ 'direction': 'rtl' }], // 文本方向
[{ 'size': ['small', false, 'large', 'huge'] }], // 字体大小
[{ 'header': [1, 2, 3, 4, 5, 6, false] }], // 标题级别(完整)
[{ 'color': [] }, { 'background': [] }], // 颜色选择
[{ 'font': [] }], // 字体选择
[{ 'align': [] }], // 对齐方式
['link', 'image', 'video'], // 链接和媒体
['clean'] // 清除格式 ],
// 方式2:使用选择器配置 // container: '#toolbar',
// 方式3:使用自定义工具栏HTML
// container: document.getElementById('custom-toolbar') }
},
placeholder: '请输入内容...'
});
Step 3:初始化配置 - 方案2
const quill = new Quill('#editor', {
theme: 'snow', // 选择主题
modules: {
toolbar: {
// 使用选择器配置(或者document.getElementById('custom-toolbar'))
container: '#toolbar',
}
},
placeholder: '请输入内容...'
});
.custom-toolbar {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
}
.custom-toolbar .ql-formats {
margin-right: 15px;
display: flex;
align-items: center;
}
.custom-toolbar button {
border: 1px solid #ddd;
border-radius: 5px;
padding: 5px 10px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-toolbar button:hover {
background: #e9ecef;
border-color: #adb5bd;
}
.custom-toolbar select {
border: 1px solid #ddd;
border-radius: 5px;
padding: 5px;
background: white;
}
<div id="custom-toolbar" class="toolbar-container">
<div class="custom-toolbar">
<!-- 字体和大小 -->
<span class="ql-formats">
<select class="ql-font"></select>
<select class="ql-size"></select>
</span>
<!-- 文本格式 -->
<span class="ql-formats">
<button class="ql-bold" title="粗体"></button>
<button class="ql-italic" title="斜体"></button>
<button class="ql-underline" title="下划线"></button>
<button class="ql-strike" title="删除线"></button>
</span>
<!-- 颜色 -->
<span class="ql-formats">
<select class="ql-color" title="文字颜色"></select>
<select class="ql-background" title="背景颜色"></select>
</span>
....
</div>
</div>
3. 核心 API:内容操作
// 获取 Delta 内容(推荐存储)
const delta = quill.getContents();
// 获取 HTML 内容(用于展示)
const html = quill.root.innerHTML;
// 设置内容(支持 Delta 或纯文本)
quill.setContents([{ insert: 'Hello Quill\n', attributes: { bold: true } }]);
// 插入内容(在光标位置)
const range = quill.getSelection(); // 获取光标位置
quill.insertEmbed(range.index, 'image', 'https://example.com/img.png');
// 标记文案为黄色 -- 预告:下一篇文章我们会通过AI查找文档错误,然后用这个API标记错误内容
quill.formatText(
startIndex, // 索引
endIndex, // 索引
{
background: "yellow"
},
Quill.sources.SILENT
);
// 获取选区格式
quill.getFormat(index, 1)
// 指定位置追加内容 -- 需要保持格式 (预告:下一篇我们会用这个功能将AI扩写的内容追加到指定位置)
const formats = instance.value.getFormat(
range.index + range.length - 1,
1
);
quill.insertText(index, '追加内容', formats, Quill.sources.USER);
预告
- 下一篇文章我们会通过AI查找文档错误,然后用
formatText标记错误内容
- 下一篇我们会用
insertText将AI扩写的内容追加到指定位置
- 更多内容见下一篇文章
四、核心功能实战:从汉化到媒体处理
汉化 -> 增加工具栏-图片上传 -> 自定义quill格式 -> 自定义quill属性格式
1. 汉化:让编辑器「说中文」
Quill 默认提示为英文(如工具栏按钮的 tooltip),需手动汉化:
scss为例
标题汉化
.editor-wrapper {
:deep(.ql-toolbar) {
.ql-picker.ql-header {
width: 70px;
.ql-picker-label::before,
.ql-picker-item::before {
content: "正文";
}
@for $i from 1 through 6 {
.ql-picker-label[data-value="#{$i}"]::before,
.ql-picker-item[data-value="#{$i}"]::before {
content: "标题#{$i}";
}
}
}
}
}
字体汉化
```字体汉化
.editor-wrapper {
:deep(.ql-toolbar) {
.ql-picker.ql-font {
.ql-picker-item,
.ql-picker-label {
&[data-value="SimSun"]::before {
content: "宋体";
font-family: "SimSun" !important;
}
&[data-value="SimHei"]::before {
content: "黑体";
font-family: "SimHei" !important;
}
&[data-value="KaiTi"]::before {
content: "楷体";
font-family: "KaiTi" !important;
}
&[data-value="FangSong_GB2312"]::before {
content: "仿宋_GB2312";
font-family: "FangSong_GB2312", FangSong !important;
width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 24px;
}
}
}
}
:deep(.ql-editor) {
font-family: "SimSun", "SimHei", "KaiTi", "FangSong", "Times New Roman",
sans-serif !important;
}
}
汉化思路一致,不一一列出,有需要可随时私我
2. 图片上传:从本地到服务器
默认图片按钮只能输入 URL,需重写逻辑实现本地上传:
const toolbarOptions = {
container: ['image'],
handlers: {
image: function() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
// 上传到服务器(替换为你的接口)
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
// 插入图片到编辑器
const range = quill.getSelection();
quill.insertEmbed(range.index, 'image', data.url);
});
};
input.click(); // 触发文件选择
}
}
};
3. 自定义规则:字体规则
注册字体 -> 工具栏配置 -> css适配
注册字体
import Quill from "quill";
export const useFontHook = () => {
// // 注册自定义字体
const Font: Record<string, any> = Quill.import("attributors/style/font");
Font.whitelist = [
"FangSong_GB2312",
"KaiTi_GB2312",
"FZXBSJW-GB1-0",
"FangSong",
"SimSun",
"SimHei",
"KaiTi",
"Times New Roman"
]; // 字体名称需与 CSS 定义一致
Quill.register(Font, true);
return {
Font
};
};
工具栏配置
const { Font } = useFontHook();
...
toolbar: {
container: [
[
{ size: SizeStyle.whitelist }, // 这里是自定义size
{
font: Font.whitelist
}
], // custom dropdown
]
}
css适配
同汉化部分
.editor-wrapper {
:deep(.ql-toolbar) {
.ql-picker.ql-font {
.ql-picker-item,
.ql-picker-label {
&[data-value="SimSun"]::before {
content: "宋体";
font-family: "SimSun" !important;
}
&[data-value="SimHei"]::before {
content: "黑体";
font-family: "SimHei" !important;
}
&[data-value="KaiTi"]::before {
content: "楷体";
font-family: "KaiTi" !important;
}
...
}
}
}
:deep(.ql-editor) {
font-family: "SimSun", "SimHei", "KaiTi", "FangSong", "Times New Roman",
sans-serif !important;
}
}
4. 自定义属性格式 -- 以margin,值为em为例
Quill工具栏是没有边距效果的(有text-indent,场景不一样),需要自行写格式
import Quill from "quill";
const Parchment = Quill.import("parchment");
const whitelist = ["2em", "4em", "6em", "8em"];
export function useMarginHook() {
class MarginAttributor extends Parchment.StyleAttributor {
constructor(styleName, key) {
super(styleName, key, {
scope: Parchment.Scope.BLOCK,
whitelist
});
}
add(node, value) {
// 直接验证传递的字符串是否在白名单中
if (!this.whitelist.includes(value)) return false;
return super.add(node, value);
}
}
Quill.register(
{
"formats/custom-margin-left": new MarginAttributor(
"custom-margin-left",
"margin-left"
),
"formats/custom-margin-right": new MarginAttributor(
"custom-margin-right",
"margin-right"
)
},
true
);
}
// 工具栏配置
toolbar: [
[{ 'custom-margin-left': ['2em', '4em', '6em', '8em'] }],
[{ 'custom-margin-right': ['2em', '4em', '6em', '8em'] }]
]
五、事件与扩展:深度控制编辑器
1. 事件监听:响应编辑行为
// 内容变化时触发(用于自动保存 或者 统计字数等)
quill.on('text-change', (delta, oldDelta, source) => {
if (source === 'user') { // 仅处理用户操作
console.log('内容变化:', delta);
}
});
// 光标/选择范围变化时触发(用于显示格式提示)
quill.on('selection-change', (range, oldRange, source) => {
if (range && range.length > 0) {
const text = quill.getText(range.index, range.length);
console.log('选中文本:', text);
}
});
2. 自定义格式:添加「高亮」功能
// 注册自定义格式
Quill.register({
'formats/highlight': class Highlight {
// 从 DOM 中读取格式
static formats(domNode) {
return domNode.style.backgroundColor === 'yellow' ? 'yellow' : false;
}
// 应用格式到 DOM
apply(domNode, value) {
domNode.style.backgroundColor = value === 'yellow' ? 'yellow' : '';
}
}
});
// 工具栏添加高亮按钮
const toolbarOptions = [
[{ 'highlight': 'yellow' }]
];
// 初始化编辑器
const quill = new Quill('#editor', {
modules: { toolbar: toolbarOptions },
// ...其他配置
});
3. 自定义 module - 导出文件
增加工具栏、激活配置、module配置
toolbar: {
container: [
'exportFile'
],
// 激活handlers -- 必须手动激活 - 重要!!!
handlers: {
exportFile: true
}
},
// exportFile插件的配置
exportFile: {
apiMethod: ({ htmlContent }) => {
const html = getFileTemplate(htmlContent);
downloadDocx({
html
});
}
}
模块注册与实现
useExportFilePlugin()
import Quill from "quill";
interface QuillIcons {
[key: string]: string;
exportFile?: string;
}
// 修改icon
const icons = Quill.import("ui/icons") as QuillIcons;
const uploadSVG =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="currentColor" d="M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64z"></path></svg>';
icons.exportFile = uploadSVG;
interface IApiMethodParams {
htmlContent: string;
}
// 定义类型
interface ExportFilePluginOptions {
apiMethod: (params: IApiMethodParams) => Promise<Blob>;
}
export const useExportFilePlugin = () => {
class ExportFilePlugin {
private quill: any;
private toolbar: any;
private apiMethod: (params: IApiMethodParams) => Promise<Blob>;
constructor(quill: any, options: ExportFilePluginOptions) {
this.quill = quill;
this.toolbar = quill.getModule("toolbar");
if (!options?.apiMethod) {
throw new Error("导出module必须传入apiMethod");
}
this.apiMethod = options.apiMethod;
// 添加工具栏
this.toolbar.addHandler("exportFile", this.handleExportClick.bind(this));
}
private async handleExportClick() {
try {
const htmlContent = this.quill.root.innerHTML;
if (htmlContent.trim?.() === "<p><br></p>") {
console.log("内容不能为空");
return;
}
// 使用配置的API方法
return this.apiMethod({ htmlContent });
} catch (error) {
console.error("导出失败:", error);
return Promise.reject({
error
});
}
}
}
Quill.register("modules/exportFile", ExportFilePlugin);
};
自定义module或规则原理类似,很多,不一一列出,有需要可随时私我
六、避坑指南:这些问题要注意
1. 样式冲突:编辑器样式被全局 CSS 覆盖
问题:项目中的全局样式(如 p { margin: 20px })会影响编辑器内部的段落样式,导致排版错乱。
解决:用 CSS 隔离编辑器样式,通过父级类名限制作用域:
css
/* 给编辑器容器添加类名 quill-container */
.quill-container .ql-editor p {
margin: 8px 0; /* 覆盖全局样式 */
}
.quill-container .ql-editor ul {
padding-left: 20px;
}
2. 图片上传:跨域问题导致插入失败
问题:上传图片到第三方服务器时,因跨域限制导致 fetch 请求失败。
解决:
- 后端接口添加 CORS 头(
Access-Control-Allow-Origin: *)。
- 若无法修改后端,通过本地服务端代理转发请求:
// 前端请求本地代理接口
fetch('/proxy/upload', { method: 'POST', body: formData })
// 本地服务端将 /proxy/upload 转发到第三方服务器
3. 自定义模块:配置后不生效
问题:如“导出模块”配置后,工具栏按钮无响应。
核心原因:2.x版本中,自定义工具栏按钮需在handlers中手动激活。
解决方案:在toolbar配置中添加handlers激活项:
解决:
modules: {
toolbar: {
container: ['exportFile'], // 自定义按钮
// 必须手动激活,否则按钮点击无响应
handlers: { exportFile: true }
},
exportFile: { /* 模块配置 */ }
}
4. 获取选中文本 得到的结果多样性
代码 instance.value.getSelection(true)
问题 调用getText()时,返回结果可能为null、空对象或空字符串,导致后续操作报错
原因 光标未在编辑器内、用户未选中内容等场景会返回不同结果。
解决方案 封装工具函数处理边界情况:
/**
* 获取选中文本 -- 只在真正有选中内容时候返回,否则返回''
* @param focus是否聚焦 - true则能获取选中内容;false则代表光标不在富文本,会返回'' (非用户触发行为除外)
* @returns obj code:-1代表没有选中 -2代表不在编辑器里 其他情况是有选中文本
*/
function getSelectionText(focus = true) {
const range = instance.value.getSelection(focus);
if (range) {
if (range.length == 0) {
console.log("用户没有选中任何内容");
return {
code: -1,
text: "",
range: {}
};
} else {
const text = instance.value.getText(range.index, range.length);
return {
code: 1,
text,
range
};
}
} else {
console.log("用户光标不在富文本编辑器里");
return {
code: -2,
text: "",
range: {}
};
}
}
5. vue、react报错 Cannot read properties of null (reading 'offsetTop')
问题 在Vue3/React项目中,初始化Quill后控制台报上述错误
原因 框架响应式系统干扰Quill内部DOM计算逻辑
解决方案
- 用非响应式变量存储
- markRaw包裹quill实例
instance.value = markRaw(new Quill('#editor'))
七 汉化效果
工具栏和下拉内容均为中文

总结与后续预告
Quill 2.x 凭借「API 驱动」「Delta 格式」「模块化设计」三大特性,成为富文本编辑器的优质选择。本文从概念解析(是什么)、原理剖析(怎么工作)到实战落地(如何使用),再到避坑指南(常见问题),覆盖了 90% 的实用场景,掌握这些内容后,你可以轻松实现博客编辑器、在线文档、评论系统等功能
下一篇预告:《AI智能写作实战:让Quill编辑器“听话”起来》
我们将深度融合AI与Quill2,实现三大核心功能:
- AI自动生成文档,填充到富文本编辑器
- AI自动检测内容错误并标记(formatText API)
- AI根据上下文扩写内容(insertText API)
- ...
资源获取
本文涉及的完整代码(含Vue3、汉化、自定义格式、自定义模块)已整理完毕,点赞+收藏+评论@我,即可私发资源包!