富文本编辑器技术选型,到底是 Prosemirror 还是 Tiptap 好 ❓❓❓

我正在开发 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 将更符合你的需求。最终的选择应基于你的开发需求、项目规模以及团队的技术能力。建议通过实际代码尝试两者,根据你的具体场景做出最适合的选择。

相关推荐
xkxnq5 小时前
第二阶段:Vue 组件化开发(第 18天)
前端·javascript·vue.js
晓得迷路了6 小时前
栗子前端技术周刊第 112 期 - Rspack 1.7、2025 JS 新星榜单、HTML 状态调查...
前端·javascript·html
怕浪猫6 小时前
React从入门到出门 第五章 React Router 配置与原理初探
前端·javascript·react.js
jinmo_C++6 小时前
从零开始学前端 · HTML 基础篇(一):认识 HTML 与页面结构
前端·html·状态模式
鹏多多6 小时前
前端2025年终总结:借着AI做大做强再创辉煌
前端·javascript
哈__6 小时前
React Native 鸿蒙跨平台开发:Vibration 实现鸿蒙端设备的震动反馈
javascript·react native·react.js
WebGISer_白茶乌龙桃6 小时前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
小Tomkk6 小时前
⭐️ StarRocks Web 使用介绍与实战指南
前端·ffmpeg
不一样的少年_6 小时前
产品催: 1 天优化 Vue 官网 SEO?我用这个插件半天搞定(不重构 Nuxt)
前端·javascript·vue.js