)
使用 React 封装 BlockSuite基于 IndexedDB 的多文档持久化实战react-indexeddb 示例深度解析【免费下载链接】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 官方示例 examples/react-indexeddb 为蓝本完整讲解如何将 BlockSuite 编辑器与文档集合DocCollection封装进 React 组件并借助浏览器原生 IndexedDB 实现「多文档 二进制更新 二进制资源Blob」的本地持久化方案。读完本文你将掌握 BlockSuite 的更新驱动update-driven持久化模型、BlobSource资源接入方式、React 上下文封装模式以及一套可直接复制到业务项目中的 IndexedDB 存储层实现。说明该示例持久化的是一个包含多篇文档的 DocCollection如果你的场景只需要保存单篇文档官方提供了更精简的 vanilla-indexeddb 示例可供参考。一、示例定位多文档集合的本地持久化在 BlockSuite 中DocCollection是承载多篇文档Doc的容器而每篇文档的编辑数据本质上是 YjsY.Doc的二进制更新流。示例 react-indexeddb 的核心设计目标非常明确在浏览器端把整个 DocCollection 的增量更新updates写入 IndexedDB页面刷新后从 IndexedDB 恢复集合与所有文档将编辑器实例与集合通过 React Context 提供给组件树使用。这与「单文档保存」的 vanilla-indexeddb 形成对照单文档只需存一份更新而集合模式必须额外维护「根文档root doc→ 子文档subdoc」的关联关系以及集合元数据collection.meta。示例基于 Vite React 18 TypeScript 构建由pnpm create vite脚手架创建见 package.json依赖idbIndexedDB 的 Promise 封装库以及 BlockSuite 的blocksuite/blocks、blocksuite/presets、blocksuite/store、blocksuite/sync四个包。项目结构速览examples/react-indexeddb/ ├── package.json # 依赖与脚本dev/build/lint/preview ├── vite.config.ts # Vite 配置 ├── index.html └── src/ ├── main.tsx # React 挂载入口 ├── App.tsx # 组件树装配 ├── index.css ├── components/ │ ├── EditorProvider.tsx # 提供 EditorContext 的 Provider │ ├── EditorContainer.tsx # 挂载编辑器 DOM 的容器 │ ├── Sidebar.tsx # 文档列表支持切换当前文档 │ └── TopBar.tsx # 顶部标题栏 └── editor/ ├── context.ts # EditorContext 与 useEditor Hook ├── editor.ts # 编辑器初始化逻辑 └── provider/ ├── db.ts # IndexedDB 客户端核心存储层 └── provider.ts # DocCollection 的持久化 Provider二、环境准备与快速启动该示例作为独立 workspace 维护在仓库 examples 目录下它不通过workspace:*引用 monorepo 内部包而是直接安装发布到 npm 的 BlockSuite canary 版本详见 examples/README.md。git clone https://github.com/toeverything/blocksuite.git cd blocksuite/examples pnpm install pnpm dev react-indexeddb命令说明pnpm install在examples这个独立 pnpm workspace 中安装全部依赖含 React、idb、BlockSuite 各包。pnpm dev react-indexeddb启动指定示例的开发服务器dev脚本映射为vite见 package.json。此外package.json 中还内置了buildtsc vite build、lint与preview脚本并提供了 StackBlitz 启动命令pnpm i pnpm dev便于在线体验。启动后浏览器会自动打开示例页面左侧为文档列表Sidebar右侧为 BlockSuite 编辑器AffineEditorContainer顶部为标题栏。你在编辑器中输入的所有内容会实时以增量更新的形式写入 IndexedDB刷新页面后数据依然存在。三、存储层设计三个 Object Store 的分工持久化的地基是 src/editor/provider/db.ts 中实现的IndexedDBClient类。它使用idb库打开数据库并在upgrade回调中创建三个 object storeStore 名称keyPath / 键策略存储内容用途docskeyPath: doc_id{ doc_id, root_doc_id }文档注册表记录每个文档的 id 及其所属根文档 id用于定位集合根updatesautoIncrement: true 索引doc_id{ doc_id, data: Uint8Array }Yjs 增量更新日志按文档追加二进制 update支持按doc_id查询blobskeyPath: blob_id{ blob_id, blob_data: Blob }二进制资源图片、附件等 Blob 数据const DB_NAME react-indexeddb; const DB_VERSION 1; const DOCS_STORE docs; const UPDATES_STORE updates; const BLOBS_STORE blobs; this.dbPromise openDB(DB_NAME, DB_VERSION, { upgrade(db) { if (!db.objectStoreNames.contains(DOCS_STORE)) { db.createObjectStore(DOCS_STORE, { keyPath: doc_id }); } if (!db.objectStoreNames.contains(UPDATES_STORE)) { const store db.createObjectStore(UPDATES_STORE, { autoIncrement: true, }); store.createIndex(doc_id, doc_id, { unique: false }); } if (!db.objectStoreNames.contains(BLOBS_STORE)) { db.createObjectStore(BLOBS_STORE, { keyPath: blob_id }); } }, });三个 store 的分工对应 BlockSuite 持久化的三类数据docs元数据只存doc_id与root_doc_id两个字段轻量且稳定。getRootDocId()通过db.getAll(DOCS_STORE)后寻找!root_doc_id的记录来定位集合根文档——这是「集合」与「单文档」持久化的关键区别单文档场景不需要这一层可对照 vanilla-indexeddb。updates增量更新每条记录是一个追加写入的 Yjs updateUint8ArrayautoIncrement保证写入顺序即应用顺序索引doc_id让getUpdates(docId)可以通过index.getAll(IDBKeyRange.only(docId))一次性取出某文档的全部历史更新。blobs二进制资源以blob_id为主键存放Blob提供insertBlob/getBlob/deleteBlob/getAllBlobIds四个方法与后文BlobSource接口一一对应。IndexedDBClient模块末尾导出单例export const client new IndexedDBClient();整个应用共享同一数据库连接。四、持久化 Provider把 Yjs 更新流接到 IndexedDB核心业务逻辑位于 src/editor/provider/provider.ts 的CollectionProvider类。它的职责是把 DocCollection 生命周期内产生的所有 Yjs 更新透明地落盘到 IndexedDB在初始化时反向回放更新重建完整集合。4.1 初始化空库建集合有库回放更新CollectionProvider.init()首先调用client.checkForExistingData()即db.count(DOCS_STORE) 0判断数据库是否有历史数据然后走两条路径static async init() { const hasData await client.checkForExistingData(); if (hasData) { return CollectionProvider._loadCollectionFromDb(); } else { return CollectionProvider._initEmptyCollection(); } }首次运行空库_initEmptyCollection生成一个随机 id${Math.random()}.slice(2, 12)用createCollection(id)创建集合注册update/subdocs监听初始化collection.meta写入根文档记录并调用createFirstDoc创建第一篇文档——它通过doc.addBlock(affine:page, {})、affine:surface、affine:note、affine:paragraph构造一个最小可编辑页面最后doc.resetHistory()清除初始化产生的历史记录。再次访问有库_loadCollectionFromDb从docsstore 读取根文档 id重建集合后先对集合自身的collection.doc回放更新_applyUpdates再遍历collection.docs对每个子文档spaceDoc回放更新并doc.load()最后重连update/subdocs监听保证后续编辑继续落盘。4.2 更新监听增量即存双向连通private _connectCollection() { const { collection } this; collection.doc.on(update, async update { await client.insertUpdate(collection.id, update); }); collection.doc.on(subdocs, subdocs { subdocs.added.forEach((doc: Y.Doc) { client.insertDoc(doc.guid, collection.id); this._connectSubDoc(doc); }); }); } private _connectSubDoc(doc: Y.Doc) { doc.on(update, async update { client.insertUpdate(doc.guid, update); }); }这段代码揭示了 BlockSuite 持久化模型的本质不保存最终快照只追加增量更新。每次编辑触发update事件携带的二进制 update 就作为一条记录追加到updatesstore新增子文档subdoc时自动在docsstore 登记映射并递归监听。回放时用DocCollection.Y.applyUpdate(doc, update)按序应用全部更新即可精确还原文档状态。这种模式天然兼容 BlockSuite 的 CRDT 数据模型也便于后续演进为 WebSocket 等实时同步可对比仓库中的 react-websocket 示例。4.3 BlobSource让图片与附件也走 IndexedDB文档中的图片、附件等二进制资源不进入 Yjs 更新流而是通过BlobSource接口管理。provider.ts 中的ClientBlobSource实现了blocksuite/sync导出的BlobSource四个方法并注入DocCollection的blobSources.mainclass ClientBlobSource implements BlobSource { readonly false; name client; async get(key: string) { return client.getBlob(key); } async set(key: string, value: Blob) { await client.insertBlob(key, value); return key; } async delete(key: string) { return client.deleteBlob(key); } async list() { return client.getAllBlobIds(); } }从源码结构可以推断BlobSource是 BlockSuite sync 模块抽象的资源读写接口将资源层与传输层解耦本地场景下把 Blob 存进 IndexedDB 的blobsstore远程场景则可以换成 HTTP 上传。getAllBlobIds对应的list()方法为未来实现资源清理垃圾回收预留了能力。五、React 集成Context 封装与编辑器挂载5.1 Context 与 Hooksrc/editor/context.ts 定义并导出EditorContext值为{ editor, provider }与useEditor()Hook。provider在模块加载时即通过export const provider await CollectionProvider.init();完成初始化顶层 await见 provider.ts并挂到window.provider便于调试。5.2 EditorProvider异步初始化的标准姿势EditorProvider.tsx 是 React 集成的核心由于编辑器与集合的初始化是异步的它用useState缓存editor与provider在useEffect中调用initEditor()后写入 state再把二者通过 Context 下发useEffect(() { initEditor().then(({ editor, provider }) { setEditor(editor); setProvider(provider); }); }, []);5.3 编辑器初始化与文档切换src/editor/editor.ts 中的initEditor()创建AffineEditorContainer来自blocksuite/presets需引入其主题样式blocksuite/presets/themes/affine.css将集合中的第一篇文档设为当前文档并订阅docLinkClicked事件实现文档间跳转export async function initEditor() { const { collection } provider; const editor new AffineEditorContainer(); const docs [...collection.docs.values()].map(blocks blocks.getDoc()); editor.doc docs[0]; editor.slots.docLinkClicked.on(({ docId }) { const target Doccollection.getDoc(docId); editor.doc target; }); return { editor, provider, collection }; }EditorContainer.tsx 负责把 Web Component 形态的编辑器实例挂载进 React 的 DOM 树useEffect中清空容器后appendChild(editor)。由于AffineEditorContainer不是 React 组件这种「ref 容器 appendChild」是官方示例采用的桥接方式。5.4 Sidebar响应式文档列表Sidebar.tsx 展示多文档管理的交互闭环通过collection.slots.docUpdated与editor.slots.docLinkClicked两个信号驱动列表刷新订阅返回dispose用于清理点击列表项时设置editor.doc doc完成当前文档切换当前文档以active类高亮标题来自doc.meta?.title || Untitled。组件装配见 App.tsxEditorProvider包裹SidebarTopBarEditorContainer形成Provider 提供状态、子组件消费状态的清晰层级入口 main.tsx 使用 React 18 的createRoot挂载。六、运行机制串联一次编辑到落盘的全链路把上面的模块串起来一次「新建文档 → 输入内容 → 刷新恢复」的完整流程是应用启动 →CollectionProvider.init()检测库状态空库则创建集合与首篇文档有库则从docsstore 找到根文档并回放updatesstore 中的全部 Yjs 更新initEditor()创建AffineEditorContainer加载首篇文档编辑器渲染完成用户输入 → Yjs 产生 update →collection.doc/ 子文档触发update事件 →client.insertUpdate追加写入updatesstore插入图片 →ClientBlobSource.set→client.insertBlob写入blobsstore新增子文档 →subdocs事件 →client.insertDoc登记docsstore 映射刷新页面 → 再次执行步骤 1文档完整恢复Sidebar 列出全部文档点击即可切换。七、总结与扩展方向通过 react-indexeddb 示例可以看到 BlockSuite 在「编辑器集成」之外的完整持久化能力面更新驱动的存储模型以 Yjs 增量更新为唯一事实来源天然支持多端合并与后续实时同步资源与文档分离BlobSource抽象让二进制资源可以按需接入不同后端框架无关的嵌入方式编辑器是标准 Web ComponentReact以及 vue-basic、svelte-basic、solid-basic、preact-basic、angular-basic 等示例只需负责宿主组件与生命周期管理。在此基础上可继续探索仓库中的进阶示例需要后端协作可参考 react-websocket需要本地 SQL 存储可参考 react-sqlite单文档轻量场景则参考 vanilla-indexeddb。需要注意的是本示例依赖发布到 npm 的 canary 版本 BlockSuite 包见 package.jsonAPI 细节可能随版本演进阅读源码时请以当前仓库版本为准。如果你正在为自己的 React 应用集成 BlockSuite 并需要浏览器本地持久化IndexedDBClientCollectionProviderBlobSource这套组合可以直接复用——只需替换DB_NAME、调整 object store 定义并挂载你自己的业务 Schema示例中使用AffineSchemas来自blocksuite/blocks即可。【免费下载链接】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),仅供参考