首页
/ Blocksuite项目中的块树操作指南

Blocksuite项目中的块树操作指南

2025-07-08 03:47:51作者:滕妙奇

前言

Blocksuite作为一个现代化的协作式编辑器框架,其核心设计理念之一就是将文档内容组织为可自由组合的块(block)结构。本文将深入解析Blocksuite中的块树(block tree)概念及其操作方法,帮助开发者更好地理解和运用这一强大的文档模型。

块树基础概念

什么是块树

在Blocksuite中,每个文档(doc)对象都管理着一棵独立的块树。这棵树由不同类型的块组成,每个块都有其特定的功能和属性。这种设计使得文档内容可以被灵活地组织和操作。

块的基本属性

每个块类型都有一个唯一的flavour标识符,采用命名空间:名称的结构。例如,Blocksuite预设编辑器中的块通常使用affine作为命名空间前缀,如affine:pageaffine:note等。

块之间的关系

块之间可以形成父子关系,构建出层次化的树形结构。这种设计使得复杂文档内容的组织变得直观且灵活。

块树操作API

Blocksuite提供了一套完整的API来操作块树,主要包括以下几个核心方法:

  1. 添加块doc.addBlock(flavour, props?, parentId?, parentIndex?)

    • 用于在指定位置添加新块
    • 可以设置块的初始属性和在父块中的位置
  2. 更新块doc.updateBlock(model, props)

    • 修改已有块的属性
    • 可以用于改变块类型或更新内容
  3. 删除块doc.deleteBlock(model)

    • 从块树中移除指定块
    • 会自动处理相关的子块
  4. 查询块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),它由三部分组成:

  1. Schema:定义块的数据结构和允许的嵌套关系
  2. Service:提供块特定的方法和属性
  3. 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)方式:

  1. 定义模型:
class MyEmbedModel extends defineEmbedModel<{
  customProp: string;
}>(BlockModel) {}
  1. 创建UI组件:
@customElement('my-embed-block')
class MyEmbedBlock extends EmbedBlockComponent<MyEmbedModel> {
  override render() {
    return html`<div>${this.model.customProp}</div>`;
  }
}
  1. 定义规范:
const MyEmbedSpec = createEmbedBlock({
  schema: {
    name: 'my-embed',
    version: 1,
    toModel: () => new MyEmbedModel(),
    props: () => ({ customProp: '' })
  },
  view: {
    component: literal`my-embed-block`
  }
});
  1. 注册到编辑器:
editor.host.specs = [...editor.host.specs, MyEmbedSpec];

结语

Blocksuite的块树设计为文档编辑提供了极大的灵活性和扩展性。通过理解块树的基本结构和操作方法,开发者可以构建出功能丰富、交互复杂的编辑器应用。无论是使用内置块类型还是开发自定义块,Blocksuite都提供了一套完整且一致的API和开发模式。

随着对块树概念的深入理解,开发者可以更好地利用Blocksuite的强大功能,打造出符合各种需求的协作编辑解决方案。