富文本编辑器选型对比(ProseMirror / TipTap vs Slate / Lexical / Quill)
返回 🗂 富文本编辑专题 · 相关私有笔记
阅读位置:这既可以是选栈前的决策入口,也可以是读完专题后的横向复盘。
- 还没选栈:先读本页的“0. 编辑器到底在抽象什么”与“9. 决策建议”;不必预先掌握 ProseMirror 的内部 API。
- 要理解取舍为什么存在:再回看 ProseMirror 入门、PM Schema 与 PM 核心模型。
本篇沿数据模型 → schema/约束 → 扩展机制 → 协同 → 框架耦合 → 上手曲线 → 生态与维护活跃度比较主流方案,最后给出“什么场景选什么”的决策建议。它只解释“为什么这样设计、这样设计带来什么”,不重复各自的内部 API 细节。
0. 先理清「编辑器」到底在抽象什么
所有现代富文本编辑器本质都在解决同一个问题:浏览器原生的 contentEditable 不可靠——它的 DOM 变更、光标、IME、撤销栈都是黑箱且跨浏览器不一致。于是各家都引入一层**自有数据模型(model)**作为「真相源」,再把 model 受控地投影到 DOM:
- 用户输入 → 拦截/解释 → 转成对 model 的变更 → 重新渲染 DOM。
- 这样撤销/重做、协同、序列化、校验都在 model 层完成,DOM 只是视图。
各家的根本分歧,就在于这个 model 长什么样、有多严格、由谁来约束。下面所有对比都可以归结到这一点。PM 一侧的「state 不可变、由 Transaction 驱动、View 受控渲染」三段式见 State 与 Transaction 与 View 与 NodeView。
1. 数据模型对比(最根本的差异)
| 方案 | 模型形态 | 可变性 | 直观心智 |
|---|---|---|---|
| ProseMirror | 严格的不可变树(Node / Mark),整数位置寻址 | 不可变,只能经 Transaction 变更 | 「带 schema 校验的文档树」 |
| TipTap | 同 ProseMirror(它就是 PM 的包装层) | 同 PM | 「声明式 Extension 编译成 PM」 |
| Lexical | 节点图 + 不可变 EditorState,双缓冲 | 不可变,update 内取 getWritable() 克隆 | 「监听器驱动的节点模型」 |
| Slate | 普通 JS 对象树 {type, children:[{text}]} | 可变,Transforms 原地改 | 「像 DOM 的可变 JSON」 |
| Quill | Delta(operation 数组)+ Parchment(Blot 树) | Delta 描述内容与 diff | 「一串 insert/attributes 操作」 |
要点说明:
- ProseMirror:文档是不可变的嵌套树,
Node表块级、Mark表行内格式;位置用整数偏移寻址。不可变性让它与 React 等框架共存安全,也让 Step / 位置映射(Mapping)这套协同机制成立。细节见 PM 核心模型。 - Lexical:状态不可变且采用双缓冲——一个 frozen 的当前态 + 一个 work-in-progress 的 pending 态,update 期间在 pending 上工作再 reconcile。节点是面向对象的子类(
ElementNode/TextNode等)。 - Slate:刻意用可变的纯 JS 对象,不依赖 immutable 库,结构镜像 DOM(
children递归)。对 React 开发者最「亲切」,但可变性是双刃剑(见 gotcha)。 - Quill:文档即 Delta——一个
{ops:[{insert, attributes}]}的 JSON 数组,既能表达内容也能表达变更(diff)。内存里由 Parchment 的 Blot 树 1:1 映射 DOM。Delta 人类可读、格式无关,但表达上存在歧义(同一段嵌套格式可有多种等价写法)。
// ProseMirror:文档是 schema 约束下的不可变树
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { schema } from 'prosemirror-schema-basic';
const state = EditorState.create({ schema });
const view = new EditorView(document.querySelector('#editor'), { state });// Quill:文档是 Delta(operation 数组)
const quill = new Quill('#editor', { theme: 'snow' });
quill.setContents([
{ insert: 'Gandalf' },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#cccccc' } },
{ insert: '\n' },
]);
console.log(quill.getContents().ops); // 取回 Delta// Slate:可变 JSON 模型,经命令式 Transforms 修改
const [editor] = useState(() => withReact(createEditor()));
Transforms.insertText(editor, 'Hello ');
// 选区/位置用 Path 数组定位:at 可接 Path、Point 或 Range
Transforms.insertText(editor, 'world', { at: { path: [0, 0], offset: 6 } });2. Schema 与约束:谁来保证文档合法
「文档能不能出现非法结构」由这一层决定,直接影响长期可维护性。
- ProseMirror —— 强 schema(最严格)。通过
new Schema({nodes, marks, topNode?})声明式定义:哪些 Node/Mark 允许出现、内容表达式(如'block+'、'inline*')规定子节点。非法组合在早期就被拒绝。代价是:内容已存在后再改 schema 会引发解析/序列化问题,必须前期想清楚。约束细节见 PM Schema。 - Lexical —— 无正式 schema。约束散落在节点子类的方法里(如
canInsertAfter())。更 OOP、更灵活,但「文档合法性」靠开发者自律。 - Slate —— schema-less。核心不带约束,扩展靠 plugin 函数与
normalizeNode归一化;permissive 但把全部责任压给开发者。 - Quill —— 介于中间。Parchment 提供标准化的 Blot 接口(松 schema),既不像 PM 那么死板,也比 Slate 有章法。
- TipTap:沿用 PM 的强 schema,只是把它藏在 Extension 背后。
// ProseMirror 自定义 schema:内容表达式即约束
import { Schema } from 'prosemirror-model';
const mySchema = new Schema({
nodes: {
doc: { content: 'block+' },
paragraph: { content: 'inline*', group: 'block', parseDOM: [{ tag: 'p' }], toDOM: () => ['p', 0] },
text: { group: 'inline' },
},
marks: {
strong: { parseDOM: [{ tag: 'strong' }], toDOM: () => ['strong', 0] },
},
});设计取舍一句话:强 schema 把错误提前到开发期(PM),弱/无 schema 把灵活性留给运行期但把风险留给团队(Slate/Lexical)。
3. 扩展机制:怎么加自定义能力
| 方案 | 扩展单元 | 心智 |
|---|---|---|
| ProseMirror | Plugin(state + props:handleDOMEvents/decorations 等) | 状态机 + 视图钩子,见 [[03 - 前端 Frontend/富文本编辑器 Rich Text Editor/Plugin 系统与 Decoration |
| TipTap | Extension / Node / Mark,声明式包装 PM plugin | editor.chain().toggleBold().run() 命令链 |
| Lexical | 节点子类 + 监听器(无统一 plugin 概念) | registerXxxListener 注册回调 |
| Slate | plugin 函数(覆写 editor 方法) | 按注册顺序串联,顺序敏感 |
| Quill | Parchment Blot + module | 自定义 Blot 注册进 registry |
- Lexical 的扩展是监听器驱动的:
registerUpdateListener()(所有更新)、registerMutationListener(NodeClass, handler)(指定节点的 created/updated/destroyed)、registerTextContentListener()。这种解耦让不需要某能力的用户不必为它付出 bundle 体积。
// Lexical:在 update 内改、在监听器里读
editor.update(() => {
const root = $getRoot();
root.append($createParagraphNode());
});
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => console.log($getRoot().getTextContent()));
});
// 针对特定节点类型的变更监听:mutations 是 Map<NodeKey, NodeMutation>
editor.registerMutationListener(MyCustomNode, (mutations) => {
for (const [key, mutation] of mutations) {
if (mutation === 'created') console.log('created', key);
if (mutation === 'destroyed') console.log('destroyed', key);
}
});- TipTap 的扩展是声明式的:一套 Extension/Node/Mark 被编译成 PM 的 schema + plugins + keymap,既给常见任务以简洁(命令链),又保留下沉到 PM 的口子。编译机制见 TipTap 架构,动手见 TipTap 实战。
// TipTap:命令链是它对 PM 命令的人体工学包装
editor.chain().focus().toggleBold().toggleItalic().run();
if (editor.can().setBold()) {
editor.chain().focus().setBold().run();
}4. 协同编辑支持:OT 还是 CRDT
这条轴往往是「能不能选它」的硬门槛。
- ProseMirror —— 原生 OT(中心化)。通过 Step /
Transaction表达原子变更,内置prosemirror-collab模块做中心化 rebasing,battle-tested,但依赖一台权威服务器做冲突解决。 - Yjs(CRDT)—— 外部但事实标准。
y-prosemirror、@lexical/yjs把编辑器绑定到 CRDT;CRDT 离线优先、可 p2p,扩展性更好,因此被 TipTap、Lexical、Slate 普遍采用。代价:PM 里 decoration 与相对位置追踪较脆弱,嵌套/异步更新要小心。 - Lexical 明确把协同外包给
@lexical/yjs,核心保持精简。 - Quill 的 Delta 天生面向 OT,但仍需服务器处理冲突。
// ProseMirror + Yjs(去中心化 CRDT):y-prosemirror 把 Y.XmlFragment 绑到 PM state
import * as Y from 'yjs';
import { ySyncPlugin, yUndoPlugin, undo, redo } from 'y-prosemirror';
import { keymap } from 'prosemirror-keymap';
const ydoc = new Y.Doc();
const plugins = [
ySyncPlugin(ydoc.getXmlFragment('prosemirror')),
yUndoPlugin(),
keymap({ 'Mod-z': undo, 'Mod-Shift-z': redo, 'Mod-y': redo }),
];
const state = EditorState.create({ schema, plugins });选型口径:离线优先 / p2p → CRDT(Yjs);服务器权威、强一致 → OT(PM 内置)。按架构选,而不是按库选。 两条路线的原理与取舍见 协同编辑。
5. 框架耦合与 headless 程度
- 框架无关:ProseMirror、Lexical 核心、Quill 都是 vanilla JS,可跨 web/mobile/server。
- 有框架绑定:TipTap 提供
@tiptap/react、@tiptap/vue、@tiptap/svelte——一套 schema 驱动多端 UI;Lexical 的@lexical/react(LexicalComposer)是可选层,核心仍是 vanilla。 - 强耦合:Slate 只支持 React,通过
<Slate>context 与useSlate()紧绑,可移植性受限。
含义:要多平台,选 ProseMirror/Quill;要框架无关又想要绑定,选 TipTap;只有当项目铁定 React-exclusive 时,Slate 的耦合才不构成问题。Lexical 核心 vanilla、React 层可选,介于两者之间。
6. 上手曲线、生态与可访问性
- 上手曲线:Quill 最快(开箱即用、
treats developers as users);TipTap 较快(命令链/StarterKit);Lexical 中等;Slate 概念简单但「坑多」,复杂结构实践上难做对;ProseMirror 最陡——Step/Transform/Mark 概念多,文档详尽但密。 - 可访问性(a11y):Lexical 显式以 WCAG 合规与屏幕阅读器兼容为目标(Meta 为消费级亿级用户而建,a11y 是硬需求,且自带原生移动绑定)。PM 处理
contentEditable基础,a11y 取决于 schema/plugin 选择;Slate 原生 a11y 薄弱,多需自行实现;Quill 取决于所用 module。a11y 实践见 性能与可访问性。 - bundle 体积:Lexical core 主打精简(只引入用到的 node/plugin),TipTap 在 PM 之上做了按需拆分,二者都更利于 code-splitting;ProseMirror、Quill 体积偏大且依赖较多。具体数字随版本与打包配置浮动,以实际构建产物为准,不在此处钉死。
7. 维护活跃度判断(截至 2026)
基于来源的现状(不臆造版本/状态):
- ProseMirror —— 活跃维护。截至 2025 年仍在更新(如
prosemirror-view1.40.1,2025-07;prosemirror-keymap1.2.3,2025-05),核心成熟稳定,长期处于 1.x 系列。 - TipTap —— 活跃开发。商业生态在扩张(Hocuspocus、TipTap Cloud),协同走 Yjs 或 TipTap Cloud(商业)。
- Lexical —— Meta 专职团队维护,但截至 2026-06 仍为 0.x,未达 1.0,API 稳定性无保证、部分 plugin 仍在成熟期;生产中有「成长期阵痛」。
- Slate —— 社区维护。生态较小、工具链有限,Android/CJK 支持弱。
- Quill —— 成熟但发布节奏偏慢,2.x 已加入 TypeScript。
- Draft.js —— 基本停更(maintenance mode),Facebook/Meta 已推荐新项目改用 Lexical。新项目不要选 Draft.js,本篇仅作历史参照。
8. 关键坑位速查(影响选型的现实约束)
- ProseMirror:schema 必须前期设计好,内容存在后再改会引发解析/序列化问题;学习曲线陡。
- Lexical:节点 reconcile 后被冻结,直接改会静默失败,必须在
editor.update()内用getWritable();没有 pure decorations——装饰会改文档,协同光标这类需求要用 HTML overlay 绕过,是已知痛点;未到 1.0。 - Slate:可变模型若不小心会触发 React 重渲染问题;plugin 按注册顺序运行、相互干扰;Android/CJK 弱(Android 上
beforeInput.preventDefault不生效)。 - Quill:Delta 表达力强但有歧义;module 系统耦合紧,换 module(如 History)需懂内部;jsdom 测试困难,推荐 Cypress。
- TipTap:高级定制仍需 PM plugin 的深入知识;Yjs 协同要手动搭。
- Yjs in PM:decoration 开销、相对位置追踪脆弱,嵌套/异步更新需谨慎。
9. 决策建议:什么场景选什么(2026)
新项目优先级:
- React + 看重可访问性 / 规模 / 移动端 → Lexical。代价:接受 0.x、部分 plugin 不成熟、协同光标需 overlay 绕过。
- 要 ProseMirror 的能力 + 框架灵活性(多框架/未来可换) → TipTap。是「想要 PM 但不想啃内核」团队的务实解,商业协同(Hocuspocus / TipTap Cloud)成熟。
- 追求简单 + 现成功能 + 快速上线 / 非 React → Quill。代价:定制受限、体积偏大。
- 需要严格 schema / 复杂版式 / 长期维护 / 最强原生协同(OT) → ProseMirror(裸用或经 TipTap)。代价:学习曲线陡。
- 避免用 Slate 做新项目(React-only、平台支持弱、协同与移动端短板),除非确为「纯 React + 简单 schema 的 greenfield 编辑器」。
- 完全避免 Draft.js(已弃用)。
正交维度提醒:
- 协同按架构选:离线优先/p2p 用 CRDT(Yjs);服务器权威用 OT。
- 多平台:ProseMirror/Quill;框架无关 + 绑定:TipTap;Slate 仅当 React-exclusive;Lexical 核心 vanilla、React 层可选。
- 本专题的栈选择:深入内核 + 强 schema + 可控协同,本仓库聚焦 ProseMirror / TipTap 一线,其余方案作为对比坐标系存在。
参考
- ProseMirror 官方文档 — https://prosemirror.net/
- ProseMirror Changelog(2025 更新) — https://prosemirror.net/docs/changelog
- TipTap 官方文档 — https://tiptap.dev/
- Lexical 官方文档 — https://lexical.dev/
- Lexical Listeners / Editor State — https://lexical.dev/docs/concepts/listeners · https://lexical.dev/docs/concepts/editor-state
- Slate 官方文档 / Transforms — https://docs.slatejs.org/ · https://docs.slatejs.org/concepts/04-transforms
- Quill 官方文档 / Delta — https://quilljs.com/ · https://quilljs.com/docs/delta
- y-prosemirror(ProseMirror × Yjs 绑定) — https://github.com/yjs/y-prosemirror
- PkgPulse: Tiptap vs Lexical vs Slate vs Quill(2026) — https://www.pkgpulse.com/guides/tiptap-vs-lexical-vs-slate-vs-quill-rich-text-editor-2026
- Eddyter: Lexical vs ProseMirror(2026) — https://eddyter.com/blogs/lexical-vs-prosemirror-2026
- Liveblocks: Which Rich Text Editor Framework(2025) — https://liveblocks.io/blog/which-rich-text-editor-framework-should-you-choose-in-2025
- Facebook Lexical Release & Draft.js Deprecation — https://news.ycombinator.com/item?id=31019778
- Draft.js 官方文档 — https://draftjs.org/docs/getting-started