☰
使用 y-partykit 在 PartyKit 上搭建 Yjs 协同后端:服务端接入、客户端 Provider 与持久化全攻略
2026/10/12 2:15:21 网站建设 项目流程
  • 后端

【免费下载链接】partykit

PartyKit simplifies developing multiplayer applications

项目地址:https://gitcode.com/gh_mirrors/pa/partykit
点击查看免费下载

y-partykit是 PartyKit 官方仓库中为 Yjs 打造的插件库(addon library),它让你用几行代码就能在 PartyKit 房间(Party/Room)上托管一个完整的 Yjs 协同后端,并内置了文档持久化、外部存储同步、只读访问与大消息分块等能力。读完本文,你将掌握:如何在 PartyKit 服务端通过onConnect接入 Yjs 协议,如何用YPartyKitProvider与useYProvider从客户端(含 React)连接房间,以及如何配置persist、load、callback等高级选项,把协同文档安全地落地到 PartyKit 存储或你自己的数据库。


一、y-partykit 是什么:把 Yjs 协同搬到 PartyKit

Yjs 是一套高性能的 CRDT 数据结构库,常用于构建协作编辑类应用(多人在线文档、画布、评论等)。它天然需要一台「同步服务器」来中转文档更新与感知(awareness)状态。y-partykit正是扮演这个角色的服务端插件——它深度 fork 了y-websocket,把 Yjs 的同步协议(y-protocols/sync、y-protocols/awareness)直接跑在 PartyKit 的房间模型上。

从仓库结构看,这个包位于 packages/y-partykit,核心源码文件如下:

  • src/index.ts:服务端入口,导出onConnect与unstable_getYDoc;
  • src/provider.ts:客户端 Provider,导出默认的YPartyKitProvider;
  • src/react.ts:React Hook 版 Provider,导出useYProvider;
  • src/storage.ts:基于 PartyKit 房间存储的持久化层YPartyKitStorage;
  • src/chunking.ts:超过 1MB 的 WebSocket 消息分块与重组。

根据 package.json 中的exports字段,该包对外提供四个子路径:主入口y-partykit、y-partykit/provider、y-partykit/storage与y-partykit/react;其运行时依赖lib0与lodash.debounce,并将yjs(^13.6.16)、y-protocols(^1.0.6)、react列为 peerDependencies(其中 react 可选)。

二、安装

在项目根目录执行:

npm install y-partykit

由于yjs、y-protocols是 peerDependencies,如果你的项目还没有安装它们,需要一并安装:

npm install yjs y-protocols

(若在浏览器端使用 React Hook,还需安装 react,但它是可选的 peer 依赖。)

三、服务端最小接入:三行代码跑通一个 Yjs 房间

y-partykit的核心导出是onConnect(conn, room, options)。在你的 PartyKit 服务端入口(通常是src/server.ts)里这样写:

// server.ts import { onConnect } from "y-partykit"; export default { async onConnect(conn, room, context) { return onConnect(conn, room); } };

仓库中的示例 examples/yjs/src/server.ts 用的是类写法,效果等价:

import type * as Party from "partykit/server"; import { onConnect } from "y-partykit"; export default class YjsServer implements Party.Server { constructor(public room: Party.Room) {} onConnect(conn: Party.Connection) { return onConnect(conn, this.room); } }

这背后发生了什么

从 src/index.ts 的onConnect实现可以看到它的完整工作流:

  1. 以room.id为键查找(或创建)一个WSSharedDoc(继承自Y.Doc的共享文档实例),并保证同一房间的并发连接复用同一个文档实例(通过getYDocPromises缓存 Promise 避免重复初始化);
  2. 把当前连接注册进doc.conns,同时为"message"与"close"事件挂上监听;
  3. 连接建立后立即向客户端发送sync step 1(携带服务端当前的状态向量),并广播当前所有在线用户的 awareness 状态。

消息处理部分(src/index.ts)遵循 Yjs 标准协议:messageSync(类型 0)用于同步文档增量,messageAwareness(类型 1)用于同步光标、在线状态等感知数据。任何客户端产生update时,服务端都会把它广播给房间内所有其他连接(updateHandler),从而保证多方编辑实时收敛。

四、客户端接入:YPartyKitProvider

服务端就绪后,客户端用 Provider 建立 WebSocket 连接并同步文档:

import YPartyKitProvider from "y-partykit/provider"; import * as Y from "yjs"; const yDoc = new Y.Doc(); const provider = new YPartyKitProvider( "localhost:1999", // PartyKit host "my-document-name", // 房间名(文档名) yDoc );

构造函数签名为new YPartyKitProvider(host, room, doc?, options?)。从 src/provider.ts 的实现可以看到几个关键细节:

  • 自动选择协议:如果host是localhost:、127.0.0.1:、192.168.、10.或私有网段172.16-31.开头,默认使用ws://,否则使用wss://;也可以通过options.protocol强制指定;
  • URL 构造:默认拼接为{ws|wss}://{host}/parties/{party|"main"}/{room},其中party可自定义(默认main),prefix可完全覆盖路由前缀;
  • 查询参数:options.params会被拼接到 WebSocket 的 query string 上,同时自动附带_pk连接 ID,常用于携带鉴权 token;
  • 延迟连接:构造函数内部会先以connect: false初始化,再在connect()中更新 URL 参数后真正建立连接。

Provider 可用选项

const provider = new YPartyKitProvider( "localhost:1999", "my-document-name", yDoc, { readOnly: false, // 只读模式:仅允许读取文档,禁止写入(默认 false) connect: false, // 不立即连接,之后手动调用 provider.connect() params: { token: "my-secret-token" }, // 追加到 WebSocket 连接 query string awareness: new awarenessProtocol.Awareness(yDoc) // 自定义 Yjs awareness 实例 } );

各选项说明:

选项类型默认值说明
connectbooleantrue是否在构造时立即连接,设为false后可手动provider.connect()
readOnlybooleanfalse只读模式,服务端会忽略该连接的写入类消息
paramsobject | () => object | Promise{}附加到连接 URL 的查询参数,支持函数/异步函数动态取值(如异步获取 token)
awarenessAwareness自动创建使用自己的 Yjs awareness 实例
protocol"ws" \| "wss"自动判断强制指定 WebSocket 协议
partystring"main"指定连接的 Party 名称
prefixstring无自定义 URL 前缀,覆盖默认/parties/:party路由
connectionIdstring随机 UUID自定义连接 ID(会作为_pk参数出现在 URL 中)
WebSocketPolyfillclass全局 WebSocket在无原生 WebSocket 的环境(如 Node)中提供 polyfill
resyncIntervalnumber-1定期重发 sync step 1 的间隔(毫秒),-1表示关闭
maxBackoffTimenumber2500断线重连的最大退避等待时间(毫秒)
disableBcboolean非浏览器环境为true是否禁用跨标签页 BroadcastChannel 同步

此外,params支持异步函数形式——这也是仓库示例 examples/yjs/src/client.ts 的用法(它用getToken()异步返回{ token }并传入params)。当params为函数时,connect()会先await其返回值,过滤掉null/undefined后再拼装 URL(见 src/provider.ts)。

Provider 同时继承了 y-websocket 的Observable事件模型,你可以监听"sync"、"synced"、"status"、"connection-error"等事件,示例中就用provider.on("sync", ...)来切换界面连接状态。

五、React 场景:useYProvider Hook

如果你在用 React,可以直接使用 Hook 版本:

import useYProvider from "y-partykit/react"; function App() { const provider = useYProvider({ host: "localhost:1999", // 可选,默认取 window.location.host room: "my-document-name", doc: yDoc, // 可选!不传则内部创建新 Y.Doc options }); }

从 src/react.ts 的实现看,useYProvider在useState中惰性创建 Provider(connect: false),随后在useEffect中调用provider.connect(),并在组件卸载时provider.disconnect(),从而把连接生命周期与组件生命周期绑定。当host未提供时,它会回退到window.location.host(非浏览器环境则用占位域名),方便部署到与前端同源的 PartyKit 域名。

六、服务端高级配置:持久化、外部存储与回调

除了最小接入,onConnect的第三个参数支持一整套选项,用来构建更复杂的后端:

// server.ts import { onConnect } from "y-partykit"; export default { async onConnect(conn, room, context) { return await onConnect(conn, room, { // 实验性:把文档持久化到 PartyKit 房间存储 persist: true, // 或者:从你自己的数据库/存储中加载文档 load() { // 从数据库或远程资源加载文档 // 返回 Y.Doc 实例(无文档则返回 null) }, callback: { async handler(yDoc) { // 编辑后每隔几秒被调用一次 // 可在这里写入数据库或外部存储 }, // 用这些选项控制 handler 的调用频率 debounceWait: 10000, // 默认 2000 ms debounceMaxWait: 20000, // 默认 10000 ms timeout: 5000 // 默认 5000 ms } }); } };

完整的选项类型YPartyKitOptions定义在 src/index.ts,包括gc、persist、callback、load、readOnly。官方文档 apps/docs/src/content/docs/reference/y-partykit-api.md 对其中的「持久化」有更详细的阐述,下面逐一展开。

6.1 默认行为:不持久化时文档随会话结束而消失

默认情况下(persist为false或未设置),PartyKit 只在至少一个客户端在线时于内存中维护一份 Yjs 文档副本;当所有客户端断开连接,文档状态就可能丢失。如果你的应用需要跨会话保留文档(比如多人文档编辑器、白板),就必须开启持久化。

6.2 持久化模式一:snapshot 快照(推荐)

onConnect(conn, room, { persist: { mode: "snapshot" } });

在snapshot模式下,PartyKit 在会话期间把增量更新逐条存入存储;当最后一个连接断开、编辑会话结束时,把所有更新合并为一份完整快照写入。它适合绝大多数不需要支持长期离线编辑的应用。

6.3 持久化模式二:history 编辑历史(进阶)

onConnect(conn, room, { persist: { mode: "history" } });

history模式保存文档的完整编辑历史,适合「多个客户端离线各自修改、之后再合并同步」的场景。但长期存活的文档,历史会无限增长,最终触及单实例的实际容量上限。因此 PartyKit 对编辑历史施加了10MB 上限,你也可以自定义这两个阈值:

onConnect(conn, room, { persist: { mode: "history", // 历史总大小上限(字节)。可设为小于 10MB(10_000_000)的任意值 maxBytes: 10_000_000, // 更新条数上限。默认不设上限,历史一直增长直到达到 maxBytes maxUpdates: 10_000 } });

一旦任一上限被触达,文档会被快照压缩,然后重新开始记录历史。在源码 src/index.ts 中可以看到:maxBytes超出 10MB 时会打印警告并回退到默认值;类型YPartyKitPersistenceStrategy(src/index.ts)定义了这两种模式的合法形态。

6.4persist: true(已废弃)

旧版本只有一个持久化开关:

onConnect(conn, room, { persist: true });

它功能上等同于{ mode: "history" }。源码中遇到persist: true时会打印弃用警告并自动转换为 history 模式(src/index.ts)。该写法仅为向后兼容保留,未来版本会移除,请优先使用显式的snapshot/history。

6.5 持久化背后的存储实现

持久化层由 src/storage.ts 实现,核心是YPartyKitStorage类。值得注意的实现细节:

  • 128KB 分块存储:PartyKit 房间存储对单个值有 128KB 上限,因此levelPut会把超过 128KB 的更新切成 128KB 一块、按前缀#序号键写入(src/storage.ts),读取时再按序拼接(levelGet);
  • 更新日志与时钟:每个文档维护单调递增的 update clock(["v1", docName, "update", clock]),storeUpdate负责追加写,getCurrentUpdateClock读取当前序号;
  • 压缩更新日志:compactUpdateLog在更新条数或总字节超过阈值时,把全部更新合并(mergeUpdates)为一条状态快照并清空旧日志——这正是「history 达到上限后自动快照」的底层机制;
  • 连接关闭落盘:当房间最后一个连接断开且开启了持久化时,服务端会调用compactUpdateLog并销毁内存中的文档实例(src/index.ts),确保状态不丢失。

6.6 对接外部数据库:load + callback 组合

如果你的业务数据存放在自己的数据库(如 Postgres、Supabase、Redis)而不是 PartyKit 存储,可以用load+callback组合实现外部持久化:

return onConnect(conn, this.room, { async load() { return await fetchDataFromExternalService(); // 返回 Y.Doc 或 null }, callback: { async handler(yDoc) { return sendDataToExternalService(yDoc); // 写回外部存储 }, // 编辑停止后 2 秒保存(默认值) debounceWait: 2000, // 若更新持续不断,至少每 10 秒保存一次(默认值) debounceMaxWait: 10000 } });

load:在房间的第一个连接建立时被调用一次,返回值会被applyUpdate到共享文档(null表示没有历史文档)。从 src/index.ts 看,load的null检查(0.0.28 版本加入)能安全处理空文档场景;文档加载后常驻内存,直到会话结束。

callback:接受handler与url两种互斥形态(类型定义见 src/index.ts,二者不能同时出现)。它的触发由lodash.debounce驱动,默认参数为:

参数默认值含义
debounceWait2000ms停止编辑后延迟多久调用 handler
debounceMaxWait10000ms持续编辑时,最多间隔多久必须调用一次
timeout5000msurl模式下 POST 请求的超时时间

当配置了callback.url时,服务端会把{ room, data }以 JSON POST 到该 URL,其中data内容由callback.objects指定(形如{ 共享对象名: "Array" | "Map" | "Text" | "XmlFragment" | "XmlElement" },对应类型由 getContent 解析);若配置了callback.handler,则直接收到Y.Doc实例。保存失败会通过console.error("failed to persist:", ...)输出,便于排查。

七、只读模式与 GC:两个容易被忽略的开关

readOnly: true会让该连接只同步文档状态、不接受任何写入。在服务端消息分发处(src/index.ts),只读连接会跳过messageYjsSyncStep2与messageYjsUpdate两类写入处理,只处理同步请求与 awareness。适合「游客围观」或「预览模式」场景。

gc(垃圾回收)用于控制 Yjs 内部结构的 GC 开关。源码中有两条默认逻辑(src/index.ts):

  • 不开启持久化时,gc默认true,让服务端内存得到更好利用;
  • 开启持久化时,gc默认false(因为 GC 会丢弃历史结构,与持久化冲突);
  • 若显式同时设置gc: true与persist,会直接抛出Cannot use gc and persist at the same time错误。

八、大文档支持:1MB 消息分块机制

PartyKit(以及底层的 Workers 平台)将单条 WebSocket 消息限制为 1MB,而大型 Yjs 文档的同步消息很容易超过这个值。src/chunking.ts 因此实现了透明的消息分块:

  • 发送端sendChunked:消息 ≤ 1MB 时原样发送;超过 1MB 时,先发一条start标记(含 id、size、count),随后按 1MB 分片逐个发送,最后发end标记(首次触发会打印一条实验性警告);
  • 接收端handleChunked:服务端在收到消息时,若为分片则缓存到收到end标记,校验 start/end 的 id、count、size 一致后重组为完整 ArrayBuffer 再交给 Yjs 处理(src/index.ts 中onConnect就是用handleChunked包裹消息监听的)。

这意味着即使文档超过 1MB,客户端与服务端之间的同步依然可用(分块对上层 Yjs 代码完全透明)。

九、unstable_getYDoc:服务端直接操作文档

如果你需要在服务端主动读取/修改某个房间的文档(例如在 HTTP 请求中返回文档内容),可以借助unstable_getYDoc(room, options)逃生舱:

import type * as Party from "partykit/server"; import type { YPartyKitOptions } from "y-partykit"; import { onConnect, unstable_getYDoc } from "y-partykit"; // options 必须与调用 onConnect 时保持一致 const opts: YPartyKitOptions = { persist: { mode: "snapshot" } }; export default class YjsServer implements Party.Server { constructor(public room: Party.Room) {} async onRequest() { const doc = await unstable_getYDoc(this.room, opts); return new Response(doc.getText("message")?.toJSON()); } onConnect(conn: Party.Connection) { return onConnect(conn, this.room, opts); } }

需要注意:该 API 标记为unstable,传入的 options 必须与onConnect一致。文档一旦初始化,后续传入的不同 options 会被忽略,源码会通过hashOptions对比并打印警告(src/index.ts)。

十、与 Yjs 生态的无缝兼容

y-partykit是y-websocket的完整 fork 与改造(见 CHANGELOG.md 中 "fully fork y-websocket" 的说明),因此 Yjs 官方文档中所有基于y-websocket的示例都能平滑迁移——只需把y-websocket替换为y-partykit/provider。任何接受「Yjs Provider」的编辑器绑定(如 Lexical、TipTap、ProseMirror 等)都可以直接使用。仓库中的 examples/lexical/src/client.tsx 就是一个现成的例子:它把YPartyKitProvider传给 Lexical 的CollaborationPlugin,服务端则只用 examples/lexical/src/server.ts 里的四行代码即可驱动整个协同编辑。

完整的可运行示例还可在 examples/yjs 找到:其 partykit.json 配置了服务端入口(src/server.ts)与静态托管(public目录 +src/client.ts构建),配合npm install后运行npx partykit dev即可在本地体验多人文本广播与在线人数感知。

总结

y-partykit用极小的接入成本,把 Yjs 的文档同步、awareness 感知、断线重连、跨标签页同步等能力与 PartyKit 的多房间、持久化存储、WebSocket 运行时深度绑定。本文覆盖了从最小服务端、客户端 Provider、React Hook,到 snapshot/history 持久化、外部存储对接、只读、GC、大消息分块与unstable_getYDoc的完整实践路径。想要深入源码,可以继续阅读 packages/y-partykit/src/index.ts、src/storage.ts 与官方参考文档 apps/docs/src/content/docs/reference/y-partykit-api.md,或直接运行仓库中的 examples/yjs 与 examples/lexical 示例体验效果。

  • 后端

【免费下载链接】partykit

PartyKit simplifies developing multiplayer applications

项目地址:https://gitcode.com/gh_mirrors/pa/partykit
点击查看免费下载
上一篇:AI for Beginners 深度学习课:使用 OpenAI Gym 与策略梯度/演员-评论家算法训练 CartPole 平衡智能体
下一篇:Linux 内核 i.MX8 DDR 性能监测单元(PMU)详解:从 perf 事件体系到 AXI ID 过滤

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询