ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

BlockSuite React WebSocket 协同编辑示例:基于 y-websocket 的文档同步与存储实战指南

2026/9/17 23:29:20 拓冰建站 浏览量
BlockSuite React WebSocket 协同编辑示例:基于 y-websocket 的文档同步与存储实战指南 BlockSuite React WebSocket 协同编辑示例基于 y-websocket 的文档同步与存储实战指南【免费下载链接】blocksuite Content editing tech stack for the web - BlockSuite is a toolkit for building editors and collaborative applications.项目地址: https://gitcode.com/GitHub_Trending/bl/blocksuite本篇技术指南围绕 BlockSuite 仓库中的 React WebSocket 示例 展开讲解如何将 BlockSuite 编辑器与文档集合Doc Collection封装进 React 组件并通过 y-websocket 后端实现多端文档同步与存储。读者将掌握pnpm create vite-express工程下的前后端启动方式、WebSocket 提供者Provider的封装思路、文档「房间」的建连与切换逻辑以及 BlockSuite 文档变更通过回调写入持久化存储的完整链路。示例概览在 React 中接入 WebSocket 同步的 BlockSuiteexamples/react-websocket是一个完整的可运行示例它将 BlockSuite 编辑器与文档集合封装在 React 应用中重点演示了基于 WebSocket 的文档同步与存储。与仅使用内存或本地 IndexedDB 的示例不同这里的文档数据通过 WebSocket 实时同步到后端并借助 y-websocket 的能力持久化到磁盘。示例的架构关系可概括为下图出自 README┌────────────┐ │ Express │ ◀──────┐ │ Server │ │ ydoc update └────────────┘ │ callback │ ┌────────────┐ ┌─────────────┐ ┌────────────┐ │ Editor │ │ Y-Websocket │ │ Document │ │ Client │◀───────────▶│ Backend │ ─────────▶│ Storage │ └────────────┘ ydoc room └─────────────┘ └────────────┘链路从左到右依次为React 中的编辑器客户端Editor Client通过「ydoc room」与 Y-Websocket 后端双向通信后端在收到更新后触发 callback将 ydoc update 通知到 Express 服务器同时文档更新被持久化到文档存储Document Storage中。WebSocket 后端由 yjs 社区的 y-websocket 提供仓库中还提到 yjs 社区同时提供带鉴权的替代方案 y-redis其仓库中的 demos 目录下也包含 BlockSuite 的接入示例。本项目使用pnpm create vite-expressCLI 创建因此同时具备 Vite 前端构建与 Express 后端能力。快速开始环境准备与一键启动示例运行依赖 pnpm 工作区。在当前仓库根目录下按以下步骤启动git clone https://github.com/toeverything/blocksuite.git cd blocksuite/examples pnpm install pnpm dev react-websocket其中pnpm dev react-websocket依赖 examples 目录下的 package.json 与 pnpm-workspace.yaml 定义的工作区脚本pnpm install会一次性安装react-websocket及其所有 workspace 依赖如blocksuite/blocks、blocksuite/presets、blocksuite/store等。启动成功后浏览器打开 Vite 默认端口即可看到编辑器界面顶部为连接状态栏左侧为文档列表All Docs右侧为主体编辑器区域。也可以直接运行cd examples/react-websocket pnpm dev在该子项目内单独启动。工程结构一次 dev 命令如何拉起前后端react-websocket的 package.json 定义了清晰的脚本编排Script说明dev用concurrently -r并行启动dev:server与ws-serverdev:servernodemon -w src/server -x tsx src/server/main.ts监听src/server目录改动并热重启 Express 服务器start:serverNODE_ENVproduction pnpm tsx src/server/main.ts以生产模式启动 Express 服务器ws-servernode --env-file .env.websocket node_modules/y-websocket/bin/server.cjs启动 y-websocket 后端进程buildvite build构建前端产物一次pnpm dev会拉起两个进程Express 服务器通过 nodemon tsx 运行 TypeScript 源码与 y-websocket 后端通过 Node 运行y-websocket/bin/server.cjs。concurrently -r的-r标志表示任一进程退出时同时终止另一个进程避免残留后台任务。前端Vite React 集成src/client 是完整的 React 前端结构如下components/EditorProvider、EditorContainer、Sidebar、TopBar四个组件editor/provider.tsWebSocket 提供者封装、editor.ts编辑器初始化、context.tsReact Context、utils.ts文档工具函数App.tsx组装整体布局main.tsx作为 React 入口。vite.config.ts 仅注册了vitejs/plugin-react插件无特殊配置项目整体遵循 Vite 5 React 18 的标准结构依赖版本见 package.json。后端Express vite-express 一体化服务src/server/main.ts 是一个极简 Express 服务器import { DocCollectionMetaState } from blocksuite/store; import express, { json } from express; import ViteExpress from vite-express; // Create http server const app express(); app.use(json()); // The data structure of the callback body is defined here. type EmptyObject Recordstring, never; type BasicWsCallbackBody { room: string; data: { meta: { type: Map; content: DocCollectionMetaState | EmptyObject; }; blocks: { type: Map; content: Recordstring, unknown | EmptyObject; }; }; }; // It is called in regular intervals when the document changes. app.post(/basic-ws-callback, async (req, res) { const { room, data } req.body as BasicWsCallbackBody; if (Object.keys(data.meta.content).length ! 0) { console.log(Meta doc in room ${room} updated); } else if (Object.keys(data.blocks.content).length ! 0) { console.log(BlockSuite doc in room ${room} updated); } res.sendStatus(200); }); // This port is the same as the port in the CALLBACK_URL in the file .env.websocket const port 5173; ViteExpress.listen(app, 5173, () console.log(Server listening at http://localhost:${port}) );关键点app.use(json())启用 JSON 中间件以解析回调请求体BasicWsCallbackBody类型精确描述了 y-websocket 回调的数据结构room为房间名data内含meta与blocks两个Map类型字段其content分别对应文档集合元数据DocCollectionMetaState与块数据回调按内容区分日志meta有更新时打印 Meta doc in room ... updated否则若blocks有更新则打印 BlockSuite doc in room ... updated端口固定为 5173注释明确指出该端口必须与.env.websocket中CALLBACK_URL的端口保持一致见下方环境变量小节。WebSocket 后端环境变量.env.websocketpnpm ws-server使用node --env-file .env.websocket加载 .env.websocket 中的配置# Basic WebSocket Host HOSTlocalhost # Basic WebSocket port PORT3001 # Persist document updates in a LevelDB database. YPERSISTENCE./storage # Basic WebSocket callback CALLBACK_URLws://localhost:5173/basic-ws-callback # Post blocks data when blocksuite document update CALLBACK_OBJECTS{meta:Map,blocks:Map}变量默认值本示例作用HOSTlocalhosty-websocket 服务监听地址PORT3001y-websocket 服务端口前端 Provider 需指向该端口YPERSISTENCE./storage开启 LevelDB 持久化将文档更新保存到./storage目录CALLBACK_URLws://localhost:5173/basic-ws-callback文档更新时回调的地址指向 Express 的/basic-ws-callback接口CALLBACK_OBJECTS{meta:Map,blocks:Map}指定回调需要投递的 Y.Map 对象meta文档元数据与blocks块数据设置YPERSISTENCE./storage后所有 WebSocket 房间的文档更新会以 LevelDB 的形式落盘关闭浏览器再打开时数据依然存在这正是「Document Storage」环节的实现基础。核心封装Provider 如何连接 WebSocket 房间前端同步逻辑的核心在 provider.ts它封装了WebsocketProvider的创建、元数据同步等待与文档房间连接。连接状态与事件插槽export type ConnectionStatus connected | disconnected | error; export class Provider { metaWs: WebsocketProvider; docWs: WebsocketProvider | null null; slots { connectStatusChanged: new SlotConnectionStatus(), docSync: new SlotDoc(), }; ... }ConnectionStatus描述三种连接状态slots使用 BlockSuite 的Slot事件总线暴露两个事件connectStatusChanged连接状态变化供 TopBar.tsx 实时显示docSync某个文档完成同步供编辑器加载文档内容。初始化先同步元数据再进入业务文档static async init(wsBaseUrl: string) { const collection initCollection(); const metaWs new WebsocketProvider( wsBaseUrl, collection.id, collection.doc ); // Make sure all document meta information is loaded. await new Promisevoid((resolve, reject) { metaWs.once(sync, () { collection.doc.load(); resolve(); }); metaWs.once(connection-error, () { reject(); }); }); return new Provider(wsBaseUrl, collection, metaWs); }init做了两件事通过initCollection()来自 utils.ts创建DocCollection——用new Schema().register(AffineSchemas)注册 BlockSuite 全部块 schema并以固定 idblocksuite-example命名集合以collection.id作为房间名创建「元数据 WebSocket 连接」等待sync事件后调用collection.doc.load()加载元数据文档从而确保所有文档的元信息在进入业务逻辑前已就绪若连接出错则 reject使初始化失败。connect按房间切换文档connect(room: string) { if (this.docWs?.roomname room) return; this.docWs?.destroy(); const doc this.collection.getDoc(room)!; this.docWs new WebsocketProvider(this.wsBaseUrl, room, doc.spaceDoc); this.docWs.on(status, (e: { status: connected | disconnected }) { this.slots.connectStatusChanged.emit(e.status); }); this.docWs.once(connection-error, () { this.slots.connectStatusChanged.emit(error); }); this.docWs.on(sync, () { this.slots.docSync.emit(doc); }); this.docWs.connect(); }connect(room)的行为若目标房间与当前docWs.roomname相同则直接返回幂等否则销毁旧连接通过collection.getDoc(room)获取对应文档并用doc.spaceDoc该文档对应的 Y.Doc创建新的WebsocketProvider监听status事件透传connected/disconnected监听一次性connection-error透传error监听sync事件通知docSync槽位显式调用connect()发起连接。这里的核心模型是每个 BlockSuite 文档对应一个 WebSocket 房间room切换文档即切换房间连接。编辑器初始化恢复会话、默认文档与事件绑定editor.ts 负责创建编辑器并绑定 Providerexport async function initEditor() { const editor new AffineEditorContainer(); // Same as .env.websocket const provider await Provider.init(ws://localhost:3001); const { collection } provider; editor.slots.docLinkClicked.on(({ docId }) { provider.connect(docId); }); provider.slots.docSync.on(doc { doc.load(); editor.doc doc; setRoom(doc.id); }); let doc: Doc | null null; const currentRoom getCurrentRoom(); if (currentRoom) { doc collection.getDoc(currentRoom); } if (doc null) { collection.docs.forEach(d { doc doc ?? d; }); } if (doc null) { doc createDoc(collection); } provider.connect(doc.id); editor.doc doc; return { editor, collection, provider }; }流程可拆解为四步创建编辑器容器实例化AffineEditorContainer来自blocksuite/presets并导入 affine.css 主题样式初始化 Provider注意ws://localhost:3001与.env.websocket中PORT3001一一对应绑定事件docLinkClicked点击文档内链接触发provider.connect(docId)切换房间docSync到达时加载文档并setRoom更新 URL选择默认文档优先恢复 URL 路径中的房间getCurrentRoom其次取集合中第一个文档最后都没有时用createDoc新建。createDocutils.ts展示了新建 BlockSuite 文档的标准骨架export function createDoc(collection: DocCollection) { const doc collection.createDoc(); doc.load(() { const pageBlockId doc.addBlock(affine:page, {}); doc.addBlock(affine:surface, {}, pageBlockId); const noteId doc.addBlock(affine:note, {}, pageBlockId); doc.addBlock(affine:paragraph, {}, noteId); }); doc.resetHistory(); return doc; }依次添加affine:page页面根块、affine:surface画布块、affine:note笔记块与affine:paragraph段落块最后resetHistory()清空初始化产生的历史记录。URL 工具函数getCurrentRoom/setRoomutils.ts通过window.location.pathname读取房间 id并用history.pushState写入编码后的房间路径使刷新页面后能恢复到同一文档。React 集成Provider 组件、编辑器挂载与侧边栏文档管理EditorProviderContext 提供初始化后的编辑器EditorProvider.tsx 在useEffect中只调用一次initEditor()用hasInitCalledref 防止 React StrictMode 下的重复初始化随后将editor、collection、provider三个对象放入 context.ts 定义的EditorContext供任意子组件通过useEditor()钩子获取。EditorContainer挂载 Web Component 编辑器useEffect(() { if (editorContainerRef.current editor) { editorContainerRef.current.innerHTML ; editorContainerRef.current.appendChild(editor); } }, [editor]);AffineEditorContainer是 Web Component 形态因此 EditorContainer.tsx 用 ref 拿到容器 div 后清空内容并appendChild(editor)将其挂载进 React 的 DOM 树。TopBar 与 Sidebar状态可视化与多文档管理TopBar.tsx 订阅connectStatusChanged将connected/disconnected/error渲染为状态文本并挂上对应 CSS 类。Sidebar.tsx 实现了文档集合的完整管理通过collection.meta.docMetas读取全部文档元信息并订阅docMetaUpdated与editor.slots.docUpdated保持列表与当前文档同步返回的 disposable 在卸载时统一disposeaddDoc用createAndInitDoc新建文档并provider.connect(doc.id)立即进入同步deleteDoc若删除的是当前文档先切换到相邻文档首删则切到第二个否则切到前一个仅剩一个时新建再调用collection.removeDoc(docId)移除。整体布局在 App.tsx 中组装EditorProvider包裹 Sidebar、TopBar 与 EditorContainer 三栏结构样式定义于 index.css 与 App.css。同步链路从编辑到回调再到落盘将各环节串起来一次编辑操作经历如下链路用户在编辑器中输入内容BlockSuite 以 Yjs CRDT 增量更新ydoc update的形式产生变更当前房间对应的WebsocketProvider将更新推送到 y-websocket 后端HOSTlocalhost:3001y-websocket 后端依据YPERSISTENCE./storage将更新写入 LevelDB完成持久化对应架构图中的 Document Storage依据CALLBACK_URL与CALLBACK_OBJECTS后端把meta、blocks两个 Y.Map 的最新内容以 POST 回调到 Express 的/basic-ws-callbackExpress 校验data.meta.content与data.blocks.content是否为空打印对应房间的更新日志并返回200确认见 main.ts其他打开同一房间room的客户端通过sync事件收到更新Provider发出docSync编辑器随之刷新——这就是多端实时同步的基础。整个示例印证了 BlockSuite 文档集合与 Yjs 生态的天然融合BlockSuite 负责编辑与文档模型y-websocket 负责网络同步LevelDB 负责持久化Express 负责应用层回调。需要生产级鉴权与更复杂部署时可参考 yjs 社区的 y-redis 方案替换后端客户端Provider的封装思路无需改动。小结examples/react-websocket是一个麻雀虽小、五脏俱全的端到端示例它演示了 BlockSuite 文档集合DocCollection的初始化、按房间room切换文档的 Provider 封装、React Context 状态管理、URL 会话恢复、侧边栏多文档增删以及 y-websocket 后端 LevelDB 持久化 Express 回调的完整同步链路。以此为模板开发者可以快速搭建基于 BlockSuite 与 WebSocket 的实时协同编辑器——唯一需要替换的就是将回调日志与本地 LevelDB 换成自己的后端存储逻辑。【免费下载链接】blocksuite Content editing tech stack for the web - BlockSuite is a toolkit for building editors and collaborative applications.项目地址: https://gitcode.com/GitHub_Trending/bl/blocksuite创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考