ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

BlockSuite 内容编辑技术栈深度指南:无头框架、CRDT 协作与组件体系

BlockSuite 内容编辑技术栈深度指南:无头框架、CRDT 协作与组件体系 BlockSuite 内容编辑技术栈深度指南无头框架、CRDT 协作与组件体系【免费下载链接】blocksuite Content editing tech stack for the web - BlockSuite is a toolkit for building editors and collaborative applications.项目地址: https://gitcode.com/GitHub_Trending/bl/blocksuiteBlockSuite 是一套用于构建编辑器与协作应用的内容编辑技术栈Content Editing Tech Stack。本文以其官方文档主页packages/docs/index.md为核心骨架系统讲解它的三大支柱——无头headless编辑器框架、可组合的 UI 组件体系、以 CRDT 为核心的原生协作能力并给出从零初始化、块树操作到自定义块的完整实战路径。读完本文你将掌握 BlockSuite 的架构分层、PageEditor/EdgelessEditor的接入方式以及如何基于BlockSpec扩展属于自己的编辑体验。一、BlockSuite 是什么People who are really serious about editor should make their own framework.BlockSuite 是一套用于构建编辑器和协作应用的工具包它独立实现了一系列内容编辑基础设施、UI 组件与编辑器本体overview。你可以把 BlockSuite 看作一个用于构建各种编辑器的 UI 组件库底层由一个极简的无头框架作为运行时支撑。借助 BlockSuite你可以做到三件事直接复用第一方编辑器PageEditor是基于块block的文档编辑器支持深度定制EdgelessEditor是支持按需开启 Canvas 渲染的图形编辑器同时与PageEditor共享相同的富文本能力page-editor、edgeless-editor。组合与扩展用丰富的 BlockSuite 组件见 components overview和示例仓库 examples 目录定制、扩展、增强这些编辑器。所有 BlockSuite 组件含编辑器都是原生 Web Components与框架无关易于和主流框架互操作。从零构建基于底层无头框架从 scratch 打造全新编辑器。BlockSuite 起源于 AFFiNE 知识库其设计目标包括支持多模态可编辑内容知识作为唯一事实来源single source of truth时其文本、幻灯片、思维导图、表格等多种视图不应依赖多套互不兼容的框架而应有一致的框架支撑。组织与可视化复杂知识现有编辑器大多聚焦单文档编辑面对相互引用交织的复杂结构力不从心因此框架需要原生管理跨多文档的状态。协作就绪实时协作不应是事后插件而应在编辑器状态管理中直接使用底层 CRDT 技术构建更清晰、更可靠的数据流见 CRDT-Native Data Flow。⚠️ BlockSuite 仍处于早期阶段组件与扩展能力还在持续打磨中文中 API 以当前仓库实际内容为准。二、整体架构框架层与组件层BlockSuite 与 AFFiNE 的关系类似于 Monaco Editor 与 VSCode但有一个关键差异BlockSuite 并非由 AFFiNE 代码自动生成而是以不同技术栈独立维护——AFFiNE 使用 React而 BlockSuite 使用 Web Components。这一选择促使 BlockSuite 在包之间划清边界保证AFFiNE 与其他项目通过组件平等地复用和扩展 BlockSuite不享有任何特权无论你使用 React 还是其他框架BlockSuite 组件都能轻松复用。据此项目被划分为两组核心包packages/framework/README.md框架层Headless Framework包名职责blocksuite/store用于建模协作文档状态的数据层原生构建在 CRDT 库 Yjs 之上为所有 BlockSuite 文档提供内置的实时协作与时间旅行time-travel能力blocksuite/inline极简的富文本组件用于行内编辑。BlockSuite 允许把不同块节点中的富文本拆分成多个 inline 编辑器让复杂内容易于组合显著降低实现传统富文本编辑特性的复杂度blocksuite/block-std框架无关的块建模库能力覆盖块字段结构、事件、选区selection、剪贴板支持等组件层Prebuilt Components包名职责blocksuite/blocks组合预设编辑器所需的默认块实现包括隶属于每个块的 widgetsblocksuite/presets即插即用的可编辑组件包含编辑器PageEditor/EdgelessEditor以及名为 fragments 的辅助 UI 组件CopilotPanel、DocTitle等blocksuite/store的 CRDT 数据层、blocksuite/inline的扁平化富文本、blocksuite/block-std的无头块建模共同构成这套既是组件库又是框架的架构基石。三、四类核心组件Editor / Fragment / Block / WidgetBlockSuite 将组件划分为四种类型component-typesEditor以各种形式呈现文档内容的容器。不同编辑器由不同的 block spec 组合而成。Block在编辑器内构建文档的原子单元。注册 block spec 后可在编辑器中渲染多个对应块实例。Widget按需在编辑器中情境化出现的辅助组件如搜索栏、取色器。每个块都可定义自己的 widgets。Fragment编辑器之外的外部组件。它们与编辑器共享文档但拥有各自的生命周期。Editor 与 Fragment 的区别在于复杂度与功能Fragment 通常提供更简化的能力、服务于特定 UI 目的而 Editor 提供对块树的完整编辑能力但两者共享相似的数据流见 CRDT-Native Data Flow。Block 与 Widget 的关系编辑器被架构为多个可编辑块的组装体每个块 spec 封装数据 schema、视图、服务与逻辑在每个块 spec 内可以有该块专属的 WidgetBlockSuite 正是借助这一机制在页面编辑器中注册了拖拽手柄drag handle、斜杠菜单slash menu等动态 UIblock-widgets。用块组合编辑器type Editor BlockSpec[]在 BlockSuite 中编辑器被设计得极其轻量真正的可编辑块注册到EditorHost组件上——它是挂载块 UI 组件的容器。默认基于 lit 框架提供 host 实现这是一个概念上可用的编辑器component-types// 默认的 BlockSuite 可编辑块 import { PageEditorBlockSpecs } from blocksuite/blocks; // 挂载块 UI 组件的容器 import { EditorHost } from blocksuite/lit; // 操作块树的 store import { type Doc } from blocksuite/store; // 标准 lit 框架原语 import { html, LitElement } from lit; import { customElement, property } from lit/decorators.js; customElement(simple-page-editor) export class SimplePageEditor extends LitElement { property({ attribute: false }) doc!: Doc; override render() { return html editor-host .doc${this.doc} .specs${PageEditorBlockSpecs} /editor-host ; } }换言之可以把 BlockSuite 编辑器理解为type Editor BlockSpec[];真实仓库中PageEditor正是这一模式的体现——它是一个自定义 Web Componentpackages/presets/src/editors/page-editor.ts内部持有EditorHost的引用并在渲染中挂载PageEditorBlockSpecs。只要存在对应的 host 实现你也可以用 React、Vue 等框架的组件模型来实现自己的 BlockSuite 编辑器framework-agnostic。一块多 Spec同一文档多种编辑体验BlockSuite 鼓励从单一块模型派生出多种块 spec 实现。例如块树根节点root block在PageEditor与EdgelessEditor中有不同的 spec 实现但共享同一个RootBlockModel两个 spec 分别作为各自编辑器的顶层 UI 组件。这让你能在同一份文档之上轻松实现多种编辑器提供多样化的编辑体验与巨大的定制潜力。四、快速上手安装与第一个编辑器从官方示例项目起步BlockSuite 兼容所有常见框架仓库 examples 提供了基于 BlockSuite 构建的 TodoMVC 风格笔记应用覆盖以下框架框架示例目录Vanilla原生 TSexamples/vanilla-indexeddbNext.jsexamples/react-basic-nextReactexamples/react-basicVueexamples/vue-basicAngularexamples/angular-basicPreactexamples/preact-basicSvelteexamples/svelte-basicSolidexamples/solid-basic例如 examples/react-basic/src/editor/editor.ts 展示了标准的初始化流程先构造Schema并注册AffineSchemas再创建DocCollection与doc在doc.load()回调中addBlock构建块树最后创建AffineEditorContainer并挂载doc。在现有项目中从零集成在已有项目中安装核心包quick-startpnpm install \ blocksuite/presetscanary \ blocksuite/blockscanary \ blocksuite/storecanary要点说明blocksuite/presets包含预构建的编辑器与可选的附加 UI 组件操作 BlockSuite 文档模型与第一方块需要blocksuite/store和blocksuite/blocksBlockSuite 的canary版本基于 master 分支每日发布也被 AFFiNE 生产环境使用。随后即可开箱即用地使用预构建的PageEditor并为其挂载一个已初始化的doc实例quick-startimport blocksuite/presets/themes/affine.css; import { createEmptyDoc, PageEditor } from blocksuite/presets; import { Text } from blocksuite/store; (async () { // 以默认块树初始化编辑器 const doc createEmptyDoc().init(); const editor new PageEditor(); editor.doc doc; document.body.appendChild(editor); // 给块节点写入初始文本内容 const paragraphs doc.getBlockByFlavour(affine:paragraph); const paragraph paragraphs[0]; doc.updateBlock(paragraph, { text: new Text(Hello World!) }); })();这里的PageEditor是标准 Web Component也可以用page-editorHTML 标签复用。另一个EdgelessEditor用法类似——只需给editor挂上doc即可。从源码运行与测试如需在本地运行 BlockSuite 源码BUILDING.md确保已安装 Node.js 与 pnpm 后pnpm install pnpm dev然后可以选择多个入口localhost:5173/starter/?init推荐用于本地调试localhost:5173/starter/列出所有 starter 预设localhost:5173是包含本地优先IndexedDB 持久化与实时协作支持的完整示例。构建各包使用pnpm build。测试方面E2E 使用 Playwright、单元测试使用 vitestpnpm test以无头模式运行pnpm test -- --debug以有头模式调试BROWSERfirefox pnpm test可指定浏览器支持firefox|webkit|chromium。五、块树基础理解与操作文档模型每个doc对象管理一棵独立的块树working-with-block-tree块通过BlockSchema定义字段与允许的嵌套关系block-schema。每个块类型有唯一的block.flavour遵循namespace:name命名结构由于预设编辑器源自 AFFiNE默认可编辑块使用affine前缀。操作块树的主要 API 有doc.addBlock、doc.updateBlock、doc.deleteBlock、doc.getBlockById。示例// 第一个块将作为根 const rootId doc.addBlock(affine:page); // 以空 props 在根下插入第二个块 const props {}; const noteId doc.addBlock(affine:note, props, rootId); // 还可提供可选的 parentIndex const paragraphId doc.addBlock(affine:paragraph, props, noteId, 0); const modelA doc.root!.children[0].children[0]; const modelB doc.getBlockById(paragraphId); console.log(modelA modelB); // true // 将段落类型更新为 h1 doc.updateBlock(modelA, { type: h1 }); doc.deleteBlock(modelA);注意在把文档挂到编辑器之前必须初始化一个合法有效的文档结构这正是createEmptyDoc()之后需要init()的原因。同时块树层级是预设编辑器专属约定——在框架层面blocksuite/store并不会对第一方affine:*块做任何特殊处理你可以自由地在块树中加入不同 namespace 的块。撤销与重做所有对doc的块操作都会被自动记录可用doc.undo()和doc.redo()回退。默认情况下一定时间内的操作会被自动合并为一条记录若想在操作过程中显式添加历史记录可在块操作之间插入doc.captureSync()const rootId doc.addBlock(affine:page); const noteId doc.addBlock(affine:note, props, rootId); // 现在就捕获一条历史记录 doc.captureSync(); // ...这在一次添加多个块、但又希望逐个撤销时尤其有用。编辑器中的块树host 与 stdconst { host } editor; const { spec, selection, command } host.std;其中两个核心概念由 BlockSuite 的框架无关架构决定editor.host即EditorHost组件是挂载块 UI 组件的容器负责把块树映射到组件树这一繁重工作。无论EditorHost用什么框架实现都可以通过host.std访问同一套为可编辑块设计的无头标准库例如std.spec中包含所有已注册的BlockSpec。通常用host.spec代替host.std.spec以简化代码。六、选择块SelectionManager 与原子选区编辑器的本质是让用户动态选择并修改数据。BlockSuite 通过SelectionManager管理选区可通过std.selection或host.selection访问。例如在编辑器选中若干块后可在控制台逐行执行// 获取当前选区状态 const cached selection.value; // 清空当前选区状态 selection.clear(); // 从缓存恢复选区状态 selection.set(cached); // 尝试只设置部分选区 selection.set([cached[0]]);block-std为SelectionManager实现了若干原子选区类型如TextSelection与BlockSelection。用户当前选中的内容被自动划分为这些基本选区数据结构记录在selection.value返回的列表中通过selection.set()也可以编程方式控制编辑器的当前选区。在selection.value中不同类型的选区可同时共存。每个选区对象至少记录对应块的id和path从根块到该块的所有块 id 序列。此外还可通过group字段进一步分类——例如在PageEditor中TextSelection与BlockSelection都属于note组。上图块选区示例结构如下[ { type: block, group: note, path: [root_id, note_id, paragraph_1_id], }, { type: block, group: note, path: [root_id, note_id, paragraph_2_id], }, ];对于更复杂的原生 SelectionTextSelection通过from和to字段标记原生选区在块中的起止位置只记录各自块中行内文本序列的index与length。这种简化之所以可行是因为可编辑块使用blocksuite/inline作为富文本组件——每个块树节点的富文本内容被独立渲染到不同 inline 编辑器中消除了富文本实例之间的嵌套flat-inlines。另外整个selection.value对象在当次会话的clientId作用域内隔离。协作编辑时不同客户端之间的选区实例会实时分发通过 providers便于实现远程光标等 UI 状态。高级用法参见 Selection 文档。七、Service 与 Command编辑器内的高层封装对块树的操作往往需要进一步封装。比如根据选区 id 获取对应块模型若手写会有些样板代码function getFirstSelectedModel(host: EditorHost) { const { selection, doc } host; const firstSelection selection.value[0]; const { path } firstSelection; const leafId path[path.length - 1]; const blockModel doc.getBlockById(leafId); return blockModel; }BlockSuite 鼓励把编辑器完全拆分为不同BlockSpec这意味着编辑器全局可用的方法与属性也应落到块级别实现因此引入了BlockServiceblock-service。Service注册某类块专属的状态与方法例如不必自己实现getFirstSelectedModel可直接使用RootService上的快捷方式const rootService host.spec.getService(affine:page); // 获取选中块的模型 rootService.selectedModel; // 获取选中块的 UI 组件 rootService.selectedBlocks;getService用于获取某块 spec 对应的 service。每个 service 是普通类在host整个生命周期内以单例存在含mounted生命周期钩子。典型用途包括对作为块树根节点的块可在其 service 上注册面向应用开发者的常用编辑器 API对需要动态配置的块通过 service 传入对应选项如 image block 的上传配置对需要在编辑器加载时执行副作用如订阅键盘快捷键的块在 service 的mounted回调中操作host这样即使块尚不存在于块树中对应逻辑也会执行。Command可复用的操作链Service 示例中还使用了this.std.command即CommandManagercommand。当需要把某些操作当作变量、动态构建控制流时Command 机制尤为合适——它能把复杂操作序列记录为可复用的链并简化操作之间的上下文共享。基本用法pipe方法用于开启新的命令链tryAll方法在当前链的上下文上依次执行多个子命令getSelectedBlock、getTextSelection等命令执行实际的块树操作inline方法把命令上下文对象上的状态传递到外部或执行其他副作用run方法最终执行命令链执行后上下文被销毁。值得强调的是getSelectedBlock这类任务型命令并非由命令管理器实现而是由各块自行注册——这正是 BlockSuite 将框架相关的host与框架无关的block-std分离的体现。八、定义新块schema service view块 spec 由三大部分构成schemablock-schema、serviceblock-service与viewblock-view。其中view的定义与所用前端框架相关。以基于blocksuite/lit的PageEditor/EdgelessEditor为例用 lit 原语定义块 specimport type { BlockSpec } from blocksuite/block-std; import { literal } from lit/static-html.js; const MyBlockSpec: BlockSpec { schema: MyBlockSchema, // 用 defineBlockSchema 定义 service: MyBlockService, // 继承 BlockService // 在此定义 lit 组件 view: { component: literalmy-block-component, widgets: { myToolbar: literalmy-toolbar, myMenu: literalmy-menu, }, }, };该设计在易用性与可定制性之间取得平衡service 与 view 都围绕 schema 构建允许为同一块模型实现不同组件、服务与 widgets从而做到一块多视图例如PageEditor与EdgelessEditor对 root block 有不同的实现不同的 widget 组合例如移除全部 widgets 以组合出只读编辑器基于不同前端框架实现同一块只需为相应框架提供EditorHost中间件实现。更简单的自定义方式Embed BlockBlockSuite 还支持以更直接的方式定义最常见的自定义块——embed block。这种块不嵌套其他块完全自行管理内部区域状态。以在PageEditor中展示 GitHub 链接卡片为例源码见 packages/blocks/src/embed-github-block先定义强类型块模型import { BlockModel } from blocksuite/store; import { defineEmbedModel } from blocksuite/blocks; // 定义强类型块模型 export class EmbedGithubModel extends defineEmbedModel{ owner: string; repo: string; }(BlockModel) {}再基于该模型定义 lit UI 组件import { EmbedBlockComponent } from blocksuite/blocks; import type { EmbedGithubBlockModel } from ./embed-github-model.js; import { html } from lit; import { customElement } from lit/decorators.js; customElement(affine-embed-github-block) export class EmbedGithubBlock extends EmbedBlockComponentEmbedGithubModel { // styles... override render() { return this.renderEmbed(() { return html div classaffine-embed-github-block h3GitHub Card/h3 div${this.model.owner}/${this.model.repo}/div /div ; }); } }然后定义对应BlockSpecimport { createEmbedBlock } from blocksuite/blocks; import { EmbedGithubBlockModel } from ./embed-github-model.js; export const EmbedGithubBlockSpec createEmbedBlock({ schema: { name: github, version: 1, toModel: () new EmbedGithubModel(), props: () ({ owner: , repo: , }), }, view: { component: literalaffine-embed-github-block, }, });最后把该BlockSpec插入host.specs数组即可扩展新块类型// ... import { PageEditorBlockSpecs } from blocksuite/blocks; import { EmbedGithubBlockSpec } from ./embed-block-spec.js; const editor new PageEditor(); editor.specs [...PageEditorBlockSpecs, EmbedGithubBlockSpec]; editor.doc doc;完成后即可把新块类型插入块树const props { owner: toeverything, repo: https://github.com/toeverything/blocksuite, }; // 默认保留 affine 前缀你也可以覆盖它 doc.addBlock(affine:embed-github, props, parentId);关于跨框架支持BlockSuite 与流行编辑器框架最大的区别在于BlockSuite 没有自己的 DOM host而是实现blocksuite/lit之类的中间件把块树映射到框架组件树。因此 BlockSuite 编辑器的整个内容区原生由不同框架掌控。默认选用 lit是因为作为 Web Component 框架lit 组件树本身就是 DOM 树从而把块树 → 组件树 → DOM 树三阶段更新简化为块树 → 组件DOM树两阶段。九、数据同步Snapshot API 与文档流式传输数据同步即保存与加载文档有两条路径data-synchronization。Snapshot API类似 editor.load() 的 JSON 方案import { Job } from blocksuite/store; const { collection } doc; // 执行任务需要 job const job new Job({ collection }); // 将当前 doc 内容导出为 snapshot JSON const json await job.docToSnapshot(doc); // 将 snapshot JSON 导入到新 doc const newDoc await job.snapshotToDoc(json);Snapshot 保存的是doc块树的 JSON 表示保留其嵌套结构。BlockSuite 还在 Snapshot 之上设计了 Adapter API处理块树与 markdown、HTML 等第三方格式之间的转换具体适配器实现见 packages/blocks/src/_common/adapters。文档流式传输始终是ui f(data)与经典机制不同BlockSuite 原生支持一种可在心智上类比 React Server Components 的状态管理策略块树状态可直接作为可序列化数据从服务器或本地数据库流式传输到客户端。这种情况下服务器上存储的文档数据不再是 JSON而始终是 CRDT 的二进制表示类似 protobuf 或 RSC payload。由于块树原生由 CRDT 实现、且状态更新时 CRDT 数据总是最先更新见 CRDT-Native Data FlowBlockSuite 编辑器的块树状态完全由 CRDT 数据驱动ui f(data)这等价于每次更新 todo 列表项时先更新服务器再用服务器返回的数据更新状态。借助 CRDT 自动解决冲突的能力这个过程可以可靠地在本地完成并与远程文档同步。相比之下传统编辑器通常只支持editor.load()之类的 API更接近打了折扣的f(data)(state)模型在应对多数据源的实时协作时复杂度更高。在 BlockSuite 中数据驱动的同步策略通过 providers 实现创建新文档时只需把doc连接到某个 provider或多个 provider块树的 CRDT 数据就会通过这些 provider 同步加载已有文档时创建新的空doc并连接到对应 provider块树数据会从 provider 数据源流入import { AffineSchemas } from blocksuite/blocks; import { AffineEditorContainer } from blocksuite/presets; import { Schema } from blocksuite/store; import { DocCollection, Text } from blocksuite/store; import { IndexeddbPersistence } from y-indexeddb; const schema new Schema().register(AffineSchemas); const collection new DocCollection({ schema }); collection.meta.initialize(); // 从一个空 doc 开始 const doc collection.createDoc(); const editor new AffineEditorContainer(); editor.doc doc; document.body.append(editor); // Case 1. // 创建新 doc 时这样 init块会被自动写入 IndexedDB function createDoc() { new IndexeddbPersistence(provider-demo, doc.spaceDoc); doc.load(() { const pageBlockId doc.addBlock(affine:page, { title: new Text(Test), }); doc.addBlock(affine:surface, {}, pageBlockId); const noteId doc.addBlock(affine:note, {}, pageBlockId); doc.addBlock( affine:paragraph, { text: new Text(Hello World!) }, noteId ); }); } // Case 2. // 加载已有 doc 时只需用 provider 回调加载内容 function loadDoc() { const provider new IndexeddbPersistence(provider-demo, doc.spaceDoc); provider.on(synced, () doc.load()); }通过同时连接多个 provider文档可自动同步到多种不同的后端如 IndexedDB、WebSocket 等见 react-indexeddb、react-sqlite、react-websocket 等示例。十、CRDT 原生数据流协作为何与生俱来传统上CRDT 常被视为专精于冲突解决的技术。许多先为单用户设计的编辑器通过集成 CRDT 库实现实时协作导致两种异构模型间的双向数据流crdt-native-data-flow本地模型更新时把原生模型状态同步到 CRDT 模型远端对端更新时把 CRDT 模型解析出的数据同步回原生模型。这种双向绑定难以可靠实现且需要不小的改动同时应用层代码往往需要区分更新是否来自远端增加了复杂度。BlockSuite 的替代方案是直接以 CRDT 模型作为唯一事实来源因使用 Yjs也称YModel。这意味着无论更新来自本地还是远端都执行同一套流程先修改 YModel触发包含本次更新全部增量状态变更的Y.Event基于Y.Event更新块树中的模型节点更新块模型后发送对应 slot 事件据此更新 UI 组件。这一设计的优势是应用层代码可以完全忽略块模型的更新来自本地编辑、历史栈还是与其他用户协作只需订阅模型更新事件即可。案例删除一个段落块假设当前块树结构为RootBlock NoteBlock ParagraphBlock 0 ParagraphBlock 1 ParagraphBlock 2用户 A 选中ParagraphBlock 2并按删除键删除它此时应调用doc.deleteBlock删除该块模型实例const blockModel doc.root.children[0].children[2]; doc.deleteBlock(blockModel);BlockSuite 并不会直接修改doc.root下的块树而是首先修改底层 YBlock。CRDT 状态改变后Yjs 生成对应的Y.Event数据结构类似 git 和虚拟 DOM 中的增量补丁BlockSuite 始终以此为基准同步块模型再触发对应的 slot 事件完成 UI 更新。本例中作为ParagraphBlock 2父节点的NoteBlock会触发model.childrenUpdatedslot 事件使 UI 框架组件树中的对应组件自我刷新。由于每个子块都有 id这非常有利于配合 UI 框架常见的列表 key 优化实现按需的块组件更新。真正的威力在于如果该块树被多人并发编辑当用户 B 执行类似操作时对应更新会被 Yjs 编码并由 provider 分发。当用户 A 收到并应用用户 B 的更新时会触发与本地编辑完全相同的状态更新管线应用无需为协作场景做任何额外修改或适配便天然获得实时协作能力。单向更新流除了以 CRDT 为唯一事实来源的块树BlockSuite 还管理不需要变更历史的共享状态如每个用户光标位置的 awareness 状态以及可能不向所有用户共享的用户元数据。这些状态的管理遵循一致的单向模式完整的状态更新过程包含三个步骤UI 事件处理视图组件产生点击、拖拽等 UI 事件并触发对应回调推荐用 command 建模和复用这些交互通过命令操作状态命令可操作编辑器状态以完成 UI 更新状态驱动视图更新状态变更后通过 slot 事件通知并更新视图组件。关于 command、view、event 等概念的深入理解可参考各自文档章节。总结以 CRDT 模型为唯一事实来源应用层代码无需关心更新源于本地还是远端简化了同步并降低了复杂度——这让应用无需侵入式修改即可获得实时协作能力也是 BlockSuite 编辑器从诞生第一天起就天生协作的关键原因。回到本文主线BlockSuite 通过**无头框架store / inline / block-std与预构建组件blocks / presets**的清晰分层把内容编辑技术栈落到了可组合、可扩展、可协作的工程实践上。入门路径建议为先跑通 quick-start 示例理解 组件类型 与块树操作再按需阅读 Command、Selection、Adapter 等进阶文档最终基于BlockSpec构建属于你自己的编辑器。【免费下载链接】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),仅供参考
返回列表