@convex-dev/prosemirror-sync

中文文档
0.2.7发布于 2026/10/03Apache-2.0TypeScriptES Module近 30 天下载 46.1万

使用 Convex 组件同步 Tiptap 的ProseMirror 文档 .

中文使用文档
文档目录14 节
  1. 预先要求: Convex
  2. 安装
  3. 使用量
  4. 块注编辑器
  5. Tiptap 编辑器
  6. 页:1
  7. 配置快照解弹间隔
  8. 无符号更改警告
  9. 处理同步错误
  10. 卸载未显示的更改
  11. 创建新文档
  12. 转换文档服务器侧
  13. 未来特点
  14. 本地运行示例

Convex ProseMiror 组件

npm version

这是一个 Convex 组件 同步一个 表达器 客户之间的文档通过 a 提示 扩展( 也与 块注) (中文(简体) ).

添加同步到云中的协作编辑器 。 使用此组件的用户 可以在多个标签或设备中编辑同一文档,更改将 与云相同步. 数据存在于您的 Convex 数据库中, 可以存储 和其余的应用程序数据一起.

只要配置您的编辑器特性, 将此组件添加到您的 Convex 后端, 并使用提供的同步反应钩。 读这个 堆叠站 更多细节.

Example of editing

示例用途,见 下级 详情:

function CollaborativeEditor() {
  const sync = useBlockNoteSync(api.prosemirrorSync, "some-id");
  return sync.isLoading ? (
    <p>Loading...</p>
  ) : sync.editor ? (
    <BlockNoteView editor={sync.editor} />
  ) : (
    <button onClick={() => sync.create(EMPTY_DOC)}>Create document</button>
  );
}

对于编辑器,您可以选择使用Tiptap或BlockNote. 随块注去 除非您想要大量定制编辑器.

  • Tiptap 是基于ProseMirror 的, 并保存了很多配置比较 用于直接使用ProseMiror编辑器。 它拥有丰富的生态系统 扩展, 而且非常定制 .
  • BlockNote基于Tiptap 并且有一个更好的UI 和经验出盒子, 其代价是更难扩展和定制高级使用箱。 若为 你正在寻找最简单的方式 获得所有的花哨的格式选择, 这就是它.
  • 不幸的是,虽然两者都基于ProseMirror,数据模型 不同的是,所以以后在不迁移的情况下切换编辑器并非微不足道 所以在公开发射前 可以先进行实验.

特征 :

  • 通过业务转换(OT)安全地合并客户之间的变化.
  • 简单的回放钩钩以获取初始文档并通过一个文档保持同步 Tittap 扩展名 .
  • 支持 Tiptap 和 BlockNote 两种模式 .
  • 服务器侧切入点用于授权读取、写取和快照.
  • 创建在线或离线的新文档.
  • 被跳出快照可以让新客户端避免阅读完整历史.
  • 删除旧快照和步骤的 API .
  • 转换文档服务器侧面, 方便的 AI 操作 .
  • 关闭带有未符号更改的标签时警告( 默认启用) .

见 下级 未来专题构想 捐款额 如何贡献。 找到窃听器了吗? 特性 请求? 放在这。 。 。 .

预先要求: Convex

你需要一个现有的 Convex 项目来使用组件。 Convex 是一个主机 后端平台,包括数据库、无服务器功能和多一个吨 可以学习 这里。 。 。 .

运行 npm create convex 或遵循其中任何一条 快速启动 来设置一个.

安装

安装组件包 :

npm install @convex-dev/prosemirror-sync

创建一个 convex.config.ts 应用程序中的文件 convex/ 文件夹并安装 通过调用 use数字 :

// convex/convex.config.ts
import { defineApp } from "convex/server";
import prosemirrorSync from "@convex-dev/prosemirror-sync/convex.config.js";

const app = defineApp();
app.use(prosemirrorSync);

export default app;

使用量

要使用该组件,请在您的文件中显示 API convex/ 文件夹,和 使用特定编辑器的同步反应钩,在引用时通过 API 您 定义。 举例来说,我们将在 convex/example.ts。 。 。 .

// convex/example.ts
import { components } from "./_generated/api";
import { ProsemirrorSync } from "@convex-dev/prosemirror-sync";

const prosemirrorSync = new ProsemirrorSync(components.prosemirrorSync);
export const {
  getSnapshot,
  submitSnapshot,
  latestVersion,
  getSteps,
  submitSteps,
} = prosemirrorSync.syncApi({
  // ...
});

在您的 React 组件中,您可以使用编辑器特定的钩子获取 通过 Tiptap 扩展文档并保持同步. 说明: 这需要 ConvexProvider 在组件树上.

块注编辑器

重要性: BlockNote 目前不支持 <StrictMode> 在反应19. 如果你在使用反弹19, 确保你删除任何 <React.StrictMode> 块 设置 reactStrictMode: false 在你身边 next.config.ts 在适用的情况下. 文档

// src/MyComponent.tsx
import { useBlockNoteSync } from "@convex-dev/prosemirror-sync/blocknote";
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { api } from "../convex/_generated/api";
import { BlockNoteEditor } from "@blocknote/core";

export function MyComponent() {
  const sync = useBlockNoteSync<BlockNoteEditor>(api.example, "some-id");
  return sync.isLoading ? (
    <p>Loading...</p>
  ) : sync.editor ? (
    <BlockNoteView editor={sync.editor} />
  ) : (
    <button onClick={() => sync.create({ type: "doc", content: [] })}>
      Create document
    </button>
  );
}

Tiptap 编辑器

在反应组件中,您可以使用 useTiptapSync 钩取 初始文档,并通过 Tiptap 扩展名保持同步 . 说明: 这个 需要一个 ConvexProvider 在组件树上.

// src/MyComponent.tsx
import { useTiptapSync } from "@convex-dev/prosemirror-sync/tiptap";
import { EditorContent, EditorProvider } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { api } from "../convex/_generated/api";

export function MyComponent() {
  const sync = useTiptapSync(api.example, "some-id");
  return sync.isLoading ? (
    <p>Loading...</p>
  ) : sync.initialContent !== null ? (
    <EditorProvider
      content={sync.initialContent}
      extensions={[StarterKit, sync.extension]}
    >
      <EditorContent editor={null} />
    </EditorProvider>
  ) : (
    <button onClick={() => sync.create({ type: "doc", content: [] })}>
      Create document
    </button>
  );
}

见以下实例: 实例.ts 财务报告和审定财务报表 应用程序x。 。 。 .

页:1

配置快照解弹间隔

快照解弹间隔默认为一秒. 您可以指定 与 snapshotDebounceMs 选项 useTiptapSync 或者说 useBlockNoteSync。 。 。 .

除非这两样都是真的,否则不会发出快照:

  • 文档已空闲到弹出间隔 .
  • 目前用户是最后一个进行更改的用户.

可能会有种族,但是由于每个客户都会提交自己的快照 改变,它们不会相互冲突 并且是安全的应用.

无符号更改警告

默认情况下,同步扩展显示用户时的浏览器确认对话框 尝试关闭未发送到服务器的本地更改标签 . 可禁用此选项 :

const sync = useTiptapSync(api.example, "some-id", {
  warnOnUnsyncedClose: false,
});

处理同步错误

过 onSyncError 当同步失败时被告知 :

const sync = useTiptapSync(api.example, "some-id", {
  onSyncError: (error) => {
    console.error("Sync failed", error);
  },
});

这还包括在背景中进行的工作,没有呼叫者 宣传失败: 解开的快照提交和冲出 编辑器被破坏时未确认的步骤(见下文).

大多数错误是信息——扩展持续同步,并重试于 服务器上的下一个本地更改或更新。 无法提交快照 特别成本无:快照只保存回放步骤,而下一个步骤 代替它.

例外的是 "Unsynced steps could not be flushed after destroy",哪个 表示本地步骤在编辑器离开前从未到达服务器. 若为 你还没通过呢 onSyncError,这个表面 是一个无手的拒绝 而不是被无声地扔下.

卸载未显示的更改

本地步骤是真理的来源,直到服务器确认为止,所以当 编辑器被销毁, 步骤仍在飞行中 您的应用程序在回合中行驶, 例如, 扩展在 而不是丢弃它们. warnOnUnsyncedClose 只覆盖关闭 选项卡,浏览器允许我们屏蔽该选项卡;无法屏蔽一个内置的未挂载.

冲出是最佳的: 如果请求失败( 离线, 说), 步骤是 损失和 onSyncError 名为. 为了完全避免这种情况 编辑器挂载到 collab.sendableSteps(editor.state) 返回无效.

创建新文档

您可以通过调用从客户端创建新文档 sync.create(content), (中文(简体) ), 或通过呼叫在服务器上 prosemirrorSync.create(ctx, id, content)。 。 。 .

内容应该是匹配 JSON 对象 司马。如果你在使用 BlockNote,它需要成为 BlockNote 的 ProseMirror JSON 代表 块。 查看存储在 snapshots 在数据库中的表格 一个例子。 两者都可以使用此值: { type: "doc", content: [] }

对于客户端文档创建:

  • 在服务器确认文件不存在之前最安全 迄今(!sync.isLoading),您可以选择在下线时使用新 创建 ID 以在您重新连接前开始编辑新文档.
  • 当客户端下次连接并同步文档时,它将提交 初始版本和所有本地更改作为步骤.
  • 如果多个客户端创建同一文档, 它会因提交而失败 不同的初始内容.
  • 注:如果在线时不打开文档,则不会同步.

转换文档服务器侧

您可以转换文档服务器侧. 它会给你最新版本 文档,您返回 螺旋变形。 。 。 .

您可以通过 new Transform(doc) 或者,如果你正在水分 一种 满 EditorState,您可以使用 Editor.create({doc}).tr 创建新的 Transaction 这是一个子类 Transform) (中文(简体) ).

例如:

import { getSchema } from "@tiptap/core";
import { EditorState } from "@tiptap/pm/state";

export const transformExample = action({
  args: {
    id: v.string(),
  },
  handler: async (ctx, { id }) => {
    const schema = getSchema(extensions);
    await prosemirrorSync.transform(ctx, id, schema, (doc) => {
      const tr = EditorState.create({ doc }).tr;
      tr.insertText("Hello, world!", 0);
      return tr;
    });
  },
});
  • 这个 extensions 应该和你的客户端编辑所用的一样, 用于任何影响计划(而不是同步扩展)的扩展.
  • 这个 transform 如果文档是 同时修改。 理想的情况是,这种回调不会做任何缓慢的操作 内部。 相反,要事先做.
  • 这个 doc 可能不同于返回的 getDoc您可以比较 version 从 getDoc 将第二个论点改为 transform 函数以查看文档是否更改.
  • 这个 transform 函数可以返回一个无效值来中止更改.
  • 如果你正在传递一个位置 插入文本, 注意修改 平行发生可能会导致情况发生变化。 还可以再过一次 或使用ProseMirror绘图能力绘制 通过取取不同版本之间的步骤在不同的版本之间位置 以 (component).lib.getSteps。 。 。 .
  • 如果你使用 BlockNote, 你需要转换 BlockNote 的块之间 BlockNote区块和ProseMirror节点. 见 块去节点和节点去Block。 。 。 .
import { Transform } from "@tiptap/pm/transform";

// An example of doing slower AI operations beforehand:
const schema = getSchema(extensions);
const { doc, version } = await prosemirrorSync.getDoc(ctx, id, schema);
const content = await generateAIContent(doc);
await prosemirrorSync.transform(ctx, id, schema, (doc, v) => {
  if (v !== version) {
    // Decide what to do if the document changes.
  }
  // An example of using Transform directly.
  const tr = new Transform(doc);
  tr.insert(0, schema.text(content));
  return tr;
});

未来特点

以后可以添加的特性 :

  • 离线编辑支持: 缓存文档和本地更改 sessionStorage 和返回在线时同步(仅用于活动浏览器标签).
    • 同时保存快照(但不是本地编辑)到 localStorage 因此新标签可以 查看并编辑下线的文档(但直到 他们回到了网上.
  • 调试同步步骤的配置( 减少函数调用) .
  • 每次提交三角洲时可选择写出具体值.
  • 用于 ReactNative 的插件存储,假设为单会话.
  • 将其转换为ProseMirror插件, 而不是 Tiptap 扩展名, 所以是原始的 ProseMirror用例也可以使用.
  • 处理边缘案件,如旧客户在较年长者身上出现局部变化 文件的版本,其中说明它们为重新定位所必须采取的步骤 从那时起,变化就被真空了.
  • 为使用 Convex 的用户输入更多类型安全的文档ID 参数 身份证作为他们的证件身份证。 这在服务器上可以使用,但不能浏览器.
  • 完全丢弃客户端ID, 只要在本地使用两个UUID来区分我们 从服务器应用的更改中更改.
  • 在数据模型中添加一个可选的 projectId来跟踪谁做了哪些修改 .

目前没有计划的缺失特征:

  • 支持大于 1兆字节的文档 .
  • 离线支持在浏览器标签或对等点之间同步更改.
  • 同步 Yjs 文档而不是ProseMirror 步骤 。 将由一个 不同Yjs特定成分.
  • 同步存在(例如在UI中显示其他用户名和光标). 这个 以Yjs为导向的ProseMiror 组件可以解决的另外一件事.
  • 回调以确认客户端的再基准和处理故障(同步期间).
  • 当一个老客户端时,优化以同步一个快照而不是许多三角洲 重新连接且没有本地化的更改.
  • 处理多个从同一云中还原的合成实例 备份,可导致多个客户端使用同一客户端ID. 现在,我们将 假设 AsyncStorage 只用于一个客户端一次.

本地运行示例

在一个终端,运行:

npm run setup
npm run example

要修改组件并自动重建:

npm run setup
npm run dev

版本与发布记录

@convex-dev/prosemirror-sync 共发布 45 个版本,首次发布于 2024/12/04;过去 12 个月发布了 8 个版本。

版本发布日期距今
0.2.7latest2026/10/031 个月前
0.2.62026/07/303 个月前
0.2.52026/06/254 个月前
0.2.42026/05/205 个月前
0.2.32026/03/078 个月前
0.2.22026/02/148 个月前

其他发布通道:alpha → 0.1.28-alpha.0。

关于 @convex-dev/prosemirror-sync 的常见问题

@convex-dev/prosemirror-sync 是什么?

使用 Convex 组件同步 Tiptap 的ProseMirror 文档 .

如何安装 @convex-dev/prosemirror-sync?

在项目目录运行 npm install @convex-dev/prosemirror-sync,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.3.1 || ^19.0.0)、convex(^1.24.8)、react-dom(^18.3.1 || ^19.0.0)、@tiptap/core(^2.7.0 || ^3.10.7)、decode-named-character-reference(^1.3.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@convex-dev/prosemirror-sync 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@convex-dev/prosemirror-sync 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@convex-dev/prosemirror-sync 可以商用吗?

@convex-dev/prosemirror-sync 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@convex-dev/prosemirror-sync 的下载量有多少?

根据 npm 官方下载统计,@convex-dev/prosemirror-sync 在 2026/10/11 – 2026/11/09 这 30 天内被下载 461,418 次,日均约 15,381 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@convex-dev/prosemirror-sync 还在维护吗?

最新版本 0.2.7 发布于 2026/10/03(1 个月前);至今共发布 45 个版本,其中过去 12 个月发布了 8 个;首次发布于 2024/12/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。