- 后端
【免费下载链接】partykit
PartyKit simplifies developing multiplayer applications
2023 年 10 月,由 StackBlitz 主办的 ViteConf 2023 把实时反应计数器嵌入了大会直播平台:观众每点一次“爱心”,全场所有人都能看到计数实时上涨。整个功能由 Vite 核心维护者 Matias Capeletto 以 PartyKit 的示例为参考、在不到一天内完成原型,大会期间累计收到23,311 次反应,其中 Evan You 的 Vite 主题演讲被点赞4.7k 次。本文以这篇 partykit-at-viteconf 博客为骨架,结合仓库中 PartyKit 的 Server 类型定义 与 Hibernation 指南,完整还原该功能的实现代码,并讲清“每个演讲一个房间”背后的 PartyKit 并发模型。读完后,你将能独立为任何线上活动、直播或内容页搭建同样的实时互动能力。
大会背景:为什么要在直播平台里加一个“点赞按钮”
线上会议最难复刻的是线下那种“共同在场”的社区氛围。ViteConf 的组织者让 Discord 与 Twitter 充满了爱、玩笑与严肃提问,并希望在大会平台本身上再增加一点互动性,让参与者真切感到“我们在一起看同一场演讲”。
于是,大会前一周,Vite 核心维护者 Matias Capeletto 基于 PartyKit 的官方示例(realtime-reaction-counter 示例文档 记录的正是这个项目)给平台加上了实时反应计数器。团队最初不确定能否在如此短的时间内落地实时功能,但借助现成示例,不到一天就做出了可运行的原型。Matias 当时的评价是:“我终于有机会试一下 PartyKit,它像魔法一样好用,真正的乐趣所在。”
效果也验证了这一点:每位观众点击按钮的瞬间,喜悦会广播给房间内的所有人,计数器实时上涨。部分演讲收获超过 2k 次反应,最受欢迎的 Evan You 主题演讲被“爱心”了 4.7k 次,大会累计反应数达到23,311 次。
核心机制:每一个演讲都对应一个独立的 PartyKit 服务器(房间)
这个功能背后最关键的设计决策是:每个演讲连接到自己的 PartyKit 服务器——在 PartyKit 的术语里,这个服务器实例叫作 “room”(房间)。下图是 ViteConf 房间的实时活动曲线:
图中的每一个尖峰都代表一场新演讲的开始——也就是大量观众同时加入某个指定的 PartyKit 房间。正因每个演讲有独立房间,各场演讲的反应数是彼此隔离、互不干扰的。
这个“按 ID 路由到同一个房间实例”的行为正是 PartyKit 的核心语义。在 how-partykit-works.md 中有明确说明:每个 PartyKit 服务器(Party)背后是一个 Cloudflare Durable Object(术语表中定义),平台根据 URL 中的id保证:
- 使用相同
id连接时,请求必然路由到同一个房间实例; - 使用新的
id连接时,平台自动为你创建新的服务器实例; - 房间以近乎零启动时间按需创建,行为类似 serverless 函数,但有状态。
因此,ViteConf 只需为每场演讲分配一个房间 ID(例如talk:{talkId}),就能获得互不干扰、各自计数、随时可扩展的一组“实时计数器”。
完整实现:整个功能只需一个 ~30 行的 Server 类
以下是博客原文给出的全部服务端代码,它构成了这个功能的全部逻辑:
import type * as Party from "partykit/server"; export default class ReactionServer implements Party.Server { options: Party.ServerOptions = { hibernate: true }; reactions: Record<string, number> = {}; constructor(readonly party: Party.Party) {} // load the reactions from built-in key-value storage async onStart() { this.reactions = (await this.party.storage.get("reactions")) ?? {}; } // when user connects, send them the current reaction counts onConnect(conn: Party.Connection) { conn.send(JSON.stringify(this.reactions)); } // when user sends a reaction, update the count onMessage(message: string) { const { kind } = JSON.parse(message); this.reactions[kind] = (this.reactions[kind] ?? 0) + 1; this.party.storage.put("reactions", this.reactions); // broadcast the updated counts to all users this.party.broadcast(JSON.stringify(this.reactions)); } }下面逐行拆解这段代码,并结合 packages/partykit/src/server.ts 中的类型定义说明每个 API 的作用。
导入与类声明
import type * as Party from "partykit/server":从partykit/server模块导入命名空间类型。在 server.ts 中,这些类型基于 Cloudflare Workers 运行时类型(DurableObjectState、DurableObjectStorage、WebSocket等)构建。export default class ReactionServer implements Party.Server:默认导出实现Party.Server接口的类。Party.Server类型在 server.ts 中定义了完整生命周期:onStart、onConnect、onMessage、onClose、onError、onRequest、onAlarm等回调均为可选实现。options: Party.ServerOptions = { hibernate: true }:声明hibernate选项(见下文“Hibernation 与并发上限”一节)。在 server.ts 中,ServerOptions只有这一个字段:是否让平台在两次 WebSocket 消息之间将服务器实例移出内存,默认值为false。
onStart:从内置键值存储恢复状态
async onStart() { this.reactions = (await this.party.storage.get("reactions")) ?? {}; }onStart在服务器首次启动、任何onConnect/onRequest之前被调用,官方注释明确说明它“适合从存储加载数据”(server.ts)。这里的party.storage是每个房间独立的键值存储(Storage extends DurableObjectStorage,见 server.ts),Room类型中的字段注释也写明“A per-room key-value storage”(server.ts)。也就是说:
- 状态天然按房间隔离,不同演讲的反应数互不串扰;
- 服务器休眠或重启后,计数可以从持久存储中恢复,不会丢失。
onConnect:新观众加入时同步当前计数
onConnect(conn: Party.Connection) { conn.send(JSON.stringify(this.reactions)); }Connection是连接到房间的 WebSocket(server.ts),带有id、send等属性。晚进场的观众需要立即看到当前计数,所以在握手完成后,服务器把完整的reactions对象推给新连接。这样每个客户端的初始状态就与服务端保持一致。
onMessage:累加、持久化、广播
onMessage(message: string) { const { kind } = JSON.parse(message); this.reactions[kind] = (this.reactions[kind] ?? 0) + 1; this.party.storage.put("reactions", this.reactions); this.party.broadcast(JSON.stringify(this.reactions)); }onMessage在收到客户端消息时触发(server.ts)。三行核心逻辑:
- 解析消息中的
kind(如"heart"),对相应类型的计数 +1; storage.put("reactions", this.reactions)将最新计数写回持久化存储——即使房间休眠、重启,计数依然保留;party.broadcast(...)把更新后的计数广播给所有已连接的客户端,Room.broadcast的签名支持字符串、ArrayBuffer、ArrayBufferView三种消息格式,并可传入without参数排除指定连接(server.ts)。这里直接广播完整对象,是因为整个reactions对象体量很小;对于数据量大的场景,应改为只广播增量。
可以看到,服务端是状态唯一权威:所有客户端都从服务端接收权威计数,而不是各自维护,天然避免了客户端间状态不一致的问题。
源码视角:Party.Server 生命周期与“状态”从何而来
理解了示例代码后,值得从仓库源码再确认几个关键设计,它们决定了这类应用的写法:
生命周期回调(server.ts):onStart→(首次)onConnect/onMessage/onRequest→onClose/onError。示例把“加载状态”放在onStart、“推送状态”放在onConnect、“变更状态”放在onMessage,正是这套生命周期的标准用法。
每个房间是一个隔离的单租户实例:Room类型(server.ts)带有id、name、storage、broadcast、getConnections、analytics等能力。id来自 Party URL(如/parties/:name/:id),是路由与隔离的依据;analytics则绑定 Cloudflare Analytics Engine 数据集——ViteConf 那张“尖峰图”正是这类每房间活动数据的可视化。
类字段即内存态:reactions: Record<string, number> = {}是挂在类实例上的内存字段。配合 how-partykit-works.md 中的“Stateful”一节,每个 Party 运行在独立的 Durable Object 中、与其他进程完全隔离,因此可以像普通 TypeScript 类一样管理状态,把每个房间当成一个单租户应用来写。这正是示例代码如此简洁的根本原因。
Hibernation 与 32,000 并发上限
博客提到“PartyKit 可以处理多达 32,000 个并发连接”。这句话的准确前提是启用 Hibernation。仓库中的 scaling-partykit-servers-with-hibernation.mdx 给出了量化对比:
- 未启用 Hibernation:每个房间最多约100 个连接;
- 启用 Hibernation:每个房间最多约32,000 个连接。
Hibernation 的本质是把连接占用的内存负担从房间进程卸载到平台:默认情况下,只要还有 WebSocket 连接,PartyKit 就会把Party.Server实例常驻内存;开启 Hibernation 后,房间在不处理消息时会休眠、实例被平台回收,但已建立的连接仍然保持,客户端毫无感知。一旦有消息到达,平台会重新实例化 Server,并再次执行构造函数与onStart(Hibernation 指南 的“How Hibernation works”一节)。
这也解释了示例代码为什么特意写了options: Party.ServerOptions = { hibernate: true }:ViteConf 全场同时可能有数千观众在线(每个演讲一个房间),必须让单房间容量从百级提升到万级。示例的写法与 Hibernation 高度契合,因为:
- 状态全部持久化在
storage中,onStart即可重建; - 消息处理不需要昂贵的外部调用;
- 数据更新是低频写入,休眠期不产生额外内存开销。
需要留意的是 Hibernation 的适用边界(同一指南文档中的说明):依赖昂贵重建状态(如每次都要调用外部 API 取数据)的场景不适合;y-partykit目前不支持 Hibernation;并且本地partykit dev环境不会休眠,开发行为可能与线上略有差异。另外,Hibernation 下onConnect里手工挂载的事件监听器会随休眠丢失,应改用onMessage/onClose回调。实践上还建议采用部分状态加载模式:不在onStart全量加载状态,而是按需读取、按多个 key 分别存储,因为storageAPI 自带内存缓存,频繁读同一 key 非常高效(Hibernation 指南 的“Partial state loading”一节)。
落地到自己的活动或产品:配置、运行与扩展方向
项目配置(partykit.json)
一个最小可用的 PartyKit 项目只需要在 partykit.json 中声明入口。以仓库中的 examples/basic 为例,关键字段包括:
{ "name": "basic", "main": "src/server.ts", "compatibilityDate": "2023-09-28", "serve": { "path": "public", "build": "src/client.ts" } }name是项目名(须匹配/^[a-z0-9_-]+$/),main指向 PartyKit 服务器入口,serve配置静态资源目录与客户端构建入口。完整的字段校验可以在 config-schema.ts 中看到:vars、define、parties、crons、build、compatibilityDate等均有对应定义。客户端则通过partysocket连接wss://.../parties/main/{roomId}即可(连接 URL 的规范见 how-partykit-works.md)。
运行与部署
在项目根目录执行partykit dev即可本地开发(注意本地不启用 Hibernation),partykit deploy部署到 PartyKit 运行时。仓库的 partykit-cli 参考文档 记录了完整的 CLI 用法。
从“计数器”到“线上活动空间”
博客给出了几个自然扩展方向,且都符合上面的并发模型:
- 动画反馈:客户端收到广播后触发点击动画,甚至可以在窗口角落营造“浮动的爱心瀑布”;
- 共创玩法:让参与者边看演讲边共同创作(写代码、画画),借助每房间的
broadcast与storage,实现难度与计数器相当; - 多房间编排:每个演讲/子页面使用独立房间 ID,平台自动按需创建实例,无需预先扩容。
更多可复制的完整示例见 examples 索引,其中 realtime-reaction-counter 正是 ViteConf 所用的官方示例(还包含 Next.js SSR 版本,服务端同时兼容两种前端)。仓库里 examples/basic、examples/react 等目录也提供了可直接运行的参照实现。
小结
从 ViteConf 2023 的实战看,PartyKit 把“为线上活动增加实时互动”压缩到了极致:每个演讲一个房间、每个房间 ~30 行服务端代码,加上hibernate: true把单房间并发上限提升到 32,000,最后用内置键值存储保证计数跨休眠持久。对任何希望给直播、发布会、教学或社区产品加上实时反馈的开发者而言,这条从示例到生产的路几乎是零成本——正如 Matias 所说,一个下午就能从零跑通原型。
- 后端
【免费下载链接】partykit
PartyKit simplifies developing multiplayer applications
相关推荐
ChatGPT Web企业级部署:SSO集成与高可用架构设计
ChatGPT Web企业级部署:SSO集成与高可用架构设计 ChatGPT Web是一款基于Express和Vue3构建的第三方ChatGPT前端页面,通过O
MediaMTX视频会议应用:基于WebRTC的多房间实时通信
MediaMTX视频会议应用:基于WebRTC的多房间实时通信 你是否还在为搭建企业级视频会议系统而烦恼?MediaMTX作为一款高性能的流媒体服务器,通过We
音视频后端免费把 200 页扫描件 PDF 转成可编辑 Markdown,MinerU 只需几分钟
免费把 200 页扫描件 PDF 转成可编辑 Markdown,MinerU 只需几分钟 一份 200 页的扫描版采购合同,附件里还有十几张价格表。你需要它变成
人工智能大模型OCR计算机视觉
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考