Blocksuite项目中的块树操作指南
前言
Blocksuite作为一个现代化的协作式编辑器框架,其核心设计理念之一就是将文档内容组织为可自由组合的块(block)结构。本文将深入解析Blocksuite中的块树(block tree)概念及其操作方法,帮助开发者更好地理解和运用这一强大的文档模型。
块树基础概念
什么是块树
在Blocksuite中,每个文档(doc)对象都管理着一棵独立的块树。这棵树由不同类型的块组成,每个块都有其特定的功能和属性。这种设计使得文档内容可以被灵活地组织和操作。
块的基本属性
每个块类型都有一个唯一的flavour
标识符,采用命名空间:名称
的结构。例如,Blocksuite预设编辑器中的块通常使用affine
作为命名空间前缀,如affine:page
、affine:note
等。
块之间的关系
块之间可以形成父子关系,构建出层次化的树形结构。这种设计使得复杂文档内容的组织变得直观且灵活。
块树操作API
Blocksuite提供了一套完整的API来操作块树,主要包括以下几个核心方法:
-
添加块:
doc.addBlock(flavour, props?, parentId?, parentIndex?)
- 用于在指定位置添加新块
- 可以设置块的初始属性和在父块中的位置
-
更新块:
doc.updateBlock(model, props)
- 修改已有块的属性
- 可以用于改变块类型或更新内容
-
删除块:
doc.deleteBlock(model)
- 从块树中移除指定块
- 会自动处理相关的子块
-
查询块:
doc.getBlockById(id)
- 根据ID获取块模型
- 用于精确查找特定块
操作示例
// 添加页面块作为根
const pageId = doc.addBlock('affine:page');
// 在页面块下添加笔记块
const noteId = doc.addBlock('affine:note', {}, pageId);
// 在笔记块中添加段落块
const paraId = doc.addBlock('affine:paragraph', {}, noteId, 0);
// 获取块模型
const paraModel = doc.getBlockById(paraId);
// 更新段落类型为标题1
doc.updateBlock(paraModel, { type: 'h1' });
// 删除段落块
doc.deleteBlock(paraModel);
撤销与重做机制
Blocksuite内置了完善的撤销/重做功能:
- 所有块操作都会被自动记录
- 可以通过
doc.undo()
和doc.redo()
进行操作回退和重做 - 默认情况下,短时间内连续的操作会被合并为一个记录
如果需要明确分隔操作记录,可以使用doc.captureSync()
:
doc.addBlock('affine:page');
doc.addBlock('affine:note', {}, pageId);
// 创建明确的撤销点
doc.captureSync();
// 后续操作将作为独立的撤销单元
doc.addBlock('affine:paragraph', {}, noteId);
编辑器环境中的块树操作
选择管理
在编辑器环境中,选择(selection)是用户交互的核心。Blocksuite通过SelectionManager
来管理选择状态:
// 获取当前选择状态
const selections = editor.host.selection.value;
// 清除选择
editor.host.selection.clear();
// 设置新的选择状态
editor.host.selection.set(newSelections);
Blocksuite支持多种选择类型,包括文本选择和块选择,这些选择可以同时存在并混合使用。
服务(Service)机制
为了更方便地操作块树,Blocksuite引入了服务(Service)概念。服务是与特定块类型关联的单例对象,提供了该类型块的专用方法和属性。
例如,通过页面块的服务可以快速获取当前选中的块:
const pageService = editor.host.spec.getService('affine:page');
// 获取选中的块模型
const selectedModels = pageService.selectedModels;
// 获取选中的块UI组件
const selectedBlocks = pageService.selectedBlocks;
命令(Command)系统
对于更复杂的操作流程,Blocksuite提供了命令系统。命令允许将多个操作组合成可重用的链:
editor.host.std.command
.pipe()
.tryAll(chain => [
chain.getTextSelection(),
chain.getBlockSelections()
])
.getSelectedBlocks()
.inline(({ selectedBlocks }) => {
// 处理选中的块
})
.run();
命令系统特别适合需要根据当前状态动态决定后续操作的场景。
自定义块开发
块规范(BlockSpec)组成
在Blocksuite中定义新块需要创建块规范(BlockSpec),它由三部分组成:
- Schema:定义块的数据结构和允许的嵌套关系
- Service:提供块特定的方法和属性
- View:定义块的UI组件和挂件
简单示例
import { BlockSpec } from '@blocksuite/block-std';
import { literal } from 'lit/static-html.js';
const MyBlockSpec: BlockSpec = {
schema: MyBlockSchema,
service: MyBlockService,
view: {
component: literal`my-block`,
widgets: {
toolbar: literal`my-toolbar`
}
}
};
嵌入式块开发
对于不需要嵌套其他块的简单块,可以使用更简便的嵌入式块(embed block)方式:
- 定义模型:
class MyEmbedModel extends defineEmbedModel<{
customProp: string;
}>(BlockModel) {}
- 创建UI组件:
@customElement('my-embed-block')
class MyEmbedBlock extends EmbedBlockComponent<MyEmbedModel> {
override render() {
return html`<div>${this.model.customProp}</div>`;
}
}
- 定义规范:
const MyEmbedSpec = createEmbedBlock({
schema: {
name: 'my-embed',
version: 1,
toModel: () => new MyEmbedModel(),
props: () => ({ customProp: '' })
},
view: {
component: literal`my-embed-block`
}
});
- 注册到编辑器:
editor.host.specs = [...editor.host.specs, MyEmbedSpec];
结语
Blocksuite的块树设计为文档编辑提供了极大的灵活性和扩展性。通过理解块树的基本结构和操作方法,开发者可以构建出功能丰富、交互复杂的编辑器应用。无论是使用内置块类型还是开发自定义块,Blocksuite都提供了一套完整且一致的API和开发模式。
随着对块树概念的深入理解,开发者可以更好地利用Blocksuite的强大功能,打造出符合各种需求的协作编辑解决方案。