我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于
Tiptap的富文本编辑器、NestJs后端服务、AI集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了Tiptap的深度定制、性能优化和协作功能的实现等核心难点。
如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。
在前端开发中,撤销和重做功能是提升用户体验的重要特性。无论是文本编辑器、图形设计工具,还是可视化搭建平台,都需要提供历史操作的回退和前进能力。这个功能看似简单,但实现起来需要考虑性能、内存占用、用户体验等多个方面。
在构建富文本编辑器时,Tiptap 和 ProseMirror 是两个常见的技术选择。两者都强大且灵活,但它们在设计理念、易用性、扩展性等方面存在差异。对于开发者来说,选择合适的工具对于项目的成功至关重要。本文将深入探讨两者的异同,并通过实际代码示例帮助你理解它们的差异,从而根据具体需求做出决策。
ProseMirror 的优势与挑战
ProseMirror 是一个 JavaScript 库,用于构建复杂的富文本编辑器。它的设计非常底层,提供了一个高效且灵活的文档模型,开发者可以完全控制编辑器的行为和界面。ProseMirror 本身并不提供任何 UI 或组件,而是一个核心库,开发者需要自行实现具体的编辑器功能。
作为一个底层框架,ProseMirror 允许开发者完全控制编辑器的各个方面,包括文档结构、输入行为、UI 样式等。它提供了丰富的 API,可以处理复杂的编辑需求,如数学公式、代码块、图片、链接等。开发者可以为几乎任何功能编写插件,并且可以在已有插件的基础上进行二次开发。基于虚拟 DOM 的设计,使其在大文档和复杂结构下能够提供较高的性能。
然而,由于其底层设计,ProseMirror 的 API 复杂,学习曲线陡峭。开发者需要深入理解其文档模型、事务管理、节点和视图的关系。由于不提供任何 UI 组件,开发者需要从零开始构建编辑器的界面和交互,配置和初始化过程也较为复杂,需要手动处理许多底层逻辑。
ProseMirror 基础使用示例
首先需要安装必要的包:
bash
npm install prosemirror-state prosemirror-view prosemirror-model prosemirror-schema-basic prosemirror-schema-list prosemirror-commands
创建一个基本的 ProseMirror 编辑器需要配置 schema、state 和 view:
javascript
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Schema, DOMParser } from "prosemirror-model";
import { schema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
import { exampleSetup } from "prosemirror-example-setup";
// 扩展基础 schema,添加列表支持
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks,
});
// 创建编辑器状态
const state = EditorState.create({
schema: mySchema,
plugins: exampleSetup({ schema: mySchema }),
});
// 创建编辑器视图
const view = new EditorView(document.querySelector("#editor"), {
state,
});
如果需要添加自定义命令,比如一个格式化工具条,需要手动实现:
javascript
import { toggleMark } from "prosemirror-commands";
import { schema } from "prosemirror-schema-basic";
// 创建加粗命令
const toggleBold = toggleMark(schema.marks.strong);
// 手动创建工具栏按钮
function createToolbar(view) {
const toolbar = document.createElement("div");
toolbar.className = "toolbar";
const boldBtn = document.createElement("button");
boldBtn.textContent = "Bold";
boldBtn.onclick = () => {
toggleBold(view.state, view.dispatch);
view.focus();
};
toolbar.appendChild(boldBtn);
return toolbar;
}
ProseMirror 自定义插件示例
创建一个自定义插件需要理解 ProseMirror 的插件系统:
javascript
import { Plugin } from "prosemirror-state";
// 创建一个字符计数插件
function characterCountPlugin() {
return new Plugin({
view(editorView) {
const counter = document.createElement("div");
counter.className = "char-counter";
const updateCounter = () => {
const text = editorView.state.doc.textContent;
counter.textContent = `字符数: ${text.length}`;
};
updateCounter();
return {
update(view) {
updateCounter();
},
destroy() {
counter.remove();
},
};
},
});
}
// 使用插件
const state = EditorState.create({
schema: mySchema,
plugins: [characterCountPlugin(), ...exampleSetup({ schema: mySchema })],
});
Tiptap 的便捷开发
Tiptap 是基于 ProseMirror 构建的富文本编辑器框架,它简化了 ProseMirror 的复杂性,提供了现成的 UI 组件和更易于使用的 API。Tiptap 旨在让开发者能够快速实现丰富的富文本编辑器,同时保持较高的灵活性和扩展性。
Tiptap 提供了简洁的 API,开发者不需要深入学习 ProseMirror 的底层概念即可实现基本的富文本编辑功能。它通过封装 ProseMirror 的复杂性,使得开发过程更加直观和简便。开箱即用的 UI 组件,如文本格式化、列表、图片插入等,极大地方便了开发者的使用,减少了开发时间。清晰的文档和活跃的开源社区,也为开发者提供了良好的支持和资源。虽然 Tiptap 进行了封装,但它仍然保留了 ProseMirror 的插件系统,开发者可以根据需要定制功能,并且可以轻松地集成其他插件。此外,Tiptap 可以与 Yjs 或其他 CRDT 库结合,支持实时协作编辑功能,这是 ProseMirror 本身不具备的特性。
不过,由于 Tiptap 封装了 ProseMirror 的很多底层功能,灵活性相对较低。对于一些需要极高自定义的需求,Tiptap 可能不如 ProseMirror 灵活。虽然在大多数情况下性能良好,但在处理超大文档或复杂操作时,性能可能不如直接使用 ProseMirror。
Tiptap 基础使用示例
Tiptap 的安装和使用相对简单:
bash
npm install @tiptap/react @tiptap/starter-kit @tiptap/pm
在 React 中使用 Tiptap:
jsx
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
function TiptapEditor() {
const editor = useEditor({
extensions: [StarterKit],
content: "<p>Hello World!</p>",
});
if (!editor) {
return null;
}
return (
<div>
<div className="toolbar">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={editor.isActive("italic") ? "is-active" : ""}
>
Italic
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive("bulletList") ? "is-active" : ""}
>
Bullet List
</button>
</div>
<EditorContent editor={editor} />
</div>
);
}
Tiptap 的 Vue 版本同样简洁:
vue
<template>
<div>
<div class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
Italic
</button>
</div>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
export default {
components: {
EditorContent,
},
setup() {
const editor = useEditor({
extensions: [StarterKit],
content: "<p>Hello World!</p>",
});
return { editor };
},
};
</script>
Tiptap 扩展功能示例
Tiptap 支持多种扩展,添加图片功能非常简单:
jsx
import Image from "@tiptap/extension-image";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
function EditorWithImage() {
const editor = useEditor({
extensions: [
StarterKit,
Image.configure({
inline: true,
allowBase64: true,
}),
],
});
const addImage = () => {
const url = window.prompt("图片URL");
if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
};
return (
<div>
<button onClick={addImage}>添加图片</button>
<EditorContent editor={editor} />
</div>
);
}
创建自定义扩展也很直观:
javascript
import { Extension } from "@tiptap/core";
import { Plugin } from "prosemirror-state";
const CharacterCount = Extension.create({
name: "characterCount",
addProseMirrorPlugins() {
return [
new Plugin({
view(editorView) {
const counter = document.createElement("div");
counter.className = "char-counter";
const updateCounter = () => {
const text = editorView.state.doc.textContent;
counter.textContent = `字符数: ${text.length}`;
};
updateCounter();
return {
update(view) {
updateCounter();
},
destroy() {
counter.remove();
},
};
},
}),
];
},
});
// 使用自定义扩展
const editor = useEditor({
extensions: [StarterKit, CharacterCount],
});
Tiptap 实时协作示例
Tiptap 与 Yjs 集成实现实时协作非常简单:
bash
npm install yjs y-prosemirror @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor
jsx
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";
// 创建 Yjs 文档和提供者
const ydoc = new Y.Doc();
const provider = new WebrtcProvider("room-name", ydoc);
function CollaborativeEditor() {
const editor = useEditor({
extensions: [
StarterKit,
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider,
}),
],
});
return <EditorContent editor={editor} />;
}
从代码看差异
让我们通过实现一个带工具栏的编辑器来对比两者的代码复杂度:
在 ProseMirror 中,需要手动管理所有状态和命令:
javascript
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "prosemirror-schema-basic";
import { toggleMark } from "prosemirror-commands";
const state = EditorState.create({ schema });
const toolbarPlugin = new Plugin({
view(editorView) {
const toolbar = document.createElement("div");
toolbar.className = "toolbar";
const boldBtn = document.createElement("button");
boldBtn.textContent = "B";
boldBtn.onclick = (e) => {
e.preventDefault();
const { state, dispatch } = editorView;
const command = toggleMark(schema.marks.strong);
if (command(state, dispatch)) {
editorView.focus();
}
};
toolbar.appendChild(boldBtn);
document.body.insertBefore(toolbar, editorView.dom);
return {
destroy() {
toolbar.remove();
},
};
},
});
const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
schema,
plugins: [toolbarPlugin],
}),
});
而在 Tiptap 中,相同的功能实现更加简洁:
jsx
const editor = useEditor({
extensions: [StarterKit],
});
return (
<div>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
B
</button>
<EditorContent editor={editor} />
</div>
);
如何做出选择
选择 Tiptap 还是 ProseMirror,关键在于项目需求和开发团队的技术能力。
如果你的目标是快速构建一个功能丰富、用户友好的富文本编辑器,且不希望花费过多时间在底层细节上,Tiptap 是一个理想的选择。它提供了简洁的 API 和现成的 UI 组件,可以快速启动和开发。如果你的编辑器需要一些定制功能,但不需要完全控制每个底层细节,Tiptap 提供了足够的灵活性,同时保持了开发的简便性。如果需要实现多人实时协作,Tiptap 内建的对 Yjs 等库的支持可以简化实现过程。
如果你需要完全控制编辑器的行为、界面和性能,ProseMirror 提供了更高的自由度。它适合那些有特定需求的项目,比如自定义文档结构、输入行为或非常复杂的编辑操作。在处理非常大的文档或需要极高性能的场景下,ProseMirror 能提供更好的优化和性能。如果你的项目需要完全自定义插件,或者你想对编辑器进行深度定制,ProseMirror 提供了更高的灵活性。
性能考虑
对于大文档处理,ProseMirror 提供了更细粒度的控制:
javascript
// ProseMirror 中可以精确控制更新
const state = EditorState.create({
schema,
plugins: [
// 可以精确控制哪些插件启用
// 可以自定义更新逻辑
new Plugin({
state: {
init() {
return {};
},
apply(tr, value) {
// 自定义状态更新逻辑
return value;
},
},
}),
],
});
而 Tiptap 虽然性能良好,但在极端场景下可能不如直接使用 ProseMirror 优化:
javascript
// Tiptap 的性能优化选项
const editor = useEditor({
extensions: [StarterKit],
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none",
},
// 可以传递 ProseMirror 的原生配置
},
// 但仍然受到封装层的限制
});
生态系统和社区支持
Tiptap 拥有丰富的扩展生态系统:
bash
# Tiptap 官方扩展
npm install @tiptap/extension-image
npm install @tiptap/extension-link
npm install @tiptap/extension-table
npm install @tiptap/extension-code-block-lowlight
npm install @tiptap/extension-placeholder
npm install @tiptap/extension-character-count
npm install @tiptap/extension-typography
而 ProseMirror 的插件需要通过 prosemirror-* 包系列来获取,或者自己实现。官方提供了基础插件,但高级功能需要社区插件或自行开发。
实际项目场景建议
对于博客平台、内容管理系统、笔记应用等常见场景,Tiptap 通常是最佳选择。它的快速开发和丰富的功能足以满足大多数需求。代码示例展示了如何在几分钟内搭建一个功能完整的编辑器。
对于需要特殊文档结构(如学术论文编辑器、代码编辑器、专业排版工具)或对性能有极致要求的场景,ProseMirror 提供了必要的底层控制能力。但需要投入更多时间学习其 API 和概念。
如果你的团队时间有限,或者希望快速迭代,Tiptap 是明智的选择。如果团队有富文本编辑器开发经验,或者有充足时间进行深度定制,ProseMirror 可以带来更高的灵活性和性能。
总结
Tiptap 是一个基于 ProseMirror 的富文本编辑器框架,适合需要快速开发、易用且功能丰富的场景。它封装了 ProseMirror 的复杂性,让开发者能够专注于业务逻辑,而无需关心底层实现细节。通过本文的代码示例可以看出,Tiptap 的 API 设计更加直观,学习曲线平缓,适合大多数项目需求。
ProseMirror 则是一个底层框架,适合那些需要完全控制文档结构、编辑行为和性能优化的高级开发者。它更灵活,但学习曲线较陡峭,适合复杂或定制化需求较强的项目。从代码示例中可以看到,使用 ProseMirror 需要处理更多的底层细节,但同时也获得了更高的控制权。
如果你的项目需要快速构建编辑器并具备一定的自定义能力,Tiptap 是一个更为理想的选择。而如果你的项目需要完全的定制化和高性能处理,ProseMirror 将更符合你的需求。最终的选择应基于你的开发需求、项目规模以及团队的技术能力。建议通过实际代码尝试两者,根据你的具体场景做出最适合的选择。