- 后端
【免费下载链接】partykit
PartyKit simplifies developing multiplayer applications
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实现可以看到它的完整工作流:
- 以
room.id为键查找(或创建)一个WSSharedDoc(继承自Y.Doc的共享文档实例),并保证同一房间的并发连接复用同一个文档实例(通过getYDocPromises缓存 Promise 避免重复初始化); - 把当前连接注册进
doc.conns,同时为"message"与"close"事件挂上监听; - 连接建立后立即向客户端发送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 实例 } );各选项说明:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
connect | boolean | true | 是否在构造时立即连接,设为false后可手动provider.connect() |
readOnly | boolean | false | 只读模式,服务端会忽略该连接的写入类消息 |
params | object | () => object | Promise | {} | 附加到连接 URL 的查询参数,支持函数/异步函数动态取值(如异步获取 token) |
awareness | Awareness | 自动创建 | 使用自己的 Yjs awareness 实例 |
protocol | "ws" \| "wss" | 自动判断 | 强制指定 WebSocket 协议 |
party | string | "main" | 指定连接的 Party 名称 |
prefix | string | 无 | 自定义 URL 前缀,覆盖默认/parties/:party路由 |
connectionId | string | 随机 UUID | 自定义连接 ID(会作为_pk参数出现在 URL 中) |
WebSocketPolyfill | class | 全局 WebSocket | 在无原生 WebSocket 的环境(如 Node)中提供 polyfill |
resyncInterval | number | -1 | 定期重发 sync step 1 的间隔(毫秒),-1表示关闭 |
maxBackoffTime | number | 2500 | 断线重连的最大退避等待时间(毫秒) |
disableBc | boolean | 非浏览器环境为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驱动,默认参数为:
| 参数 | 默认值 | 含义 |
|---|---|---|
debounceWait | 2000ms | 停止编辑后延迟多久调用 handler |
debounceMaxWait | 10000ms | 持续编辑时,最多间隔多久必须调用一次 |
timeout | 5000ms | url模式下 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
相关推荐
PartyKit 协同后端实战指南:使用 y-partykit 为 Yjs 搭建实时多人协作服务
PartyKit 协同后端实战指南:使用 y partykit 为 Yjs 搭建实时多人协作服务 y partykit 是 PartyKit 官方推出的附加库,
后端PartyKit 上的 Yjs 协同后端:y-partykit 核心机制与持久化方案深度解析
PartyKit 上的 Yjs 协同后端:y partykit 核心机制与持久化方案深度解析 y partykit 是 PartyKit 官方为 Yjs htt
后端Gemma-4-E4B-it-8bit API使用手册:开发者必知的7个核心功能
Gemma 4 E4B it 8bit API使用手册:开发者必知的7个核心功能 Gemma 4 E4B it 8bit是一款专为Apple Silicon优化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考