Convex ProseMiror 组件
这是一个 Convex 组件 同步一个 表达器 客户之间的文档通过 a 提示 扩展( 也与 块注) (中文(简体) ).
添加同步到云中的协作编辑器 。 使用此组件的用户 可以在多个标签或设备中编辑同一文档,更改将 与云相同步. 数据存在于您的 Convex 数据库中, 可以存储 和其余的应用程序数据一起.
只要配置您的编辑器特性, 将此组件添加到您的 Convex 后端, 并使用提供的同步反应钩。 读这个 堆叠站 更多细节.
示例用途,见 下级 详情:
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
