BlockSuite Note Block 深入解析:从页面文档容器到画布白板便签
2026/9/17 9:53:59 网站建设 项目流程

BlockSuite Note Block 深入解析:从页面文档容器到画布白板便签

【免费下载链接】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 中的 Note Block(affine:note)是一个承载流式文档内容的容器块,它既是页面(Doc)编辑器中全部正文的唯一宿主,也是画布(Edgeless)编辑器中可自由摆放、拆分的"便签卡片"。本文以 note-block.md 为骨架,结合 note-model.ts、note-block.ts、note-edgeless-block.ts 等源码实现,完整讲解 Note Block 的定位、xywh/index定位机制、显示模式、组件与服务层以及键盘交互,帮助你在自己的编辑器中正确使用与定制该块。

BlockSuite 页面区块嵌套关系示意图

Note Block 是什么

官方文档给出的定义非常简洁:Note Block 是用于放置流式文档内容(flowing document content)的容器块(container block)。所谓"流式内容",指的是段落、列表、代码块这类按文档流顺序排列、自上而下排版的块。与之相对的是画布上的图形元素(shape、connector 等),它们由 Surface Block 负责渲染,不参与文档流。

从块树结构看,Note Block 的嵌套关系是:

Page Block(root) ├── Surface Block(可选,用于图形编辑) └── Note Block ├── Paragraph Block │ └── Paragraph Block ├── Paragraph Block └── ...

上图清晰地展示了这一层级:Note Block 位于 Page Block 之下,其内部再承载多个 Paragraph Block。文档中的"block-nesting"示意图(block-nesting.png)即用于说明这一结构。

Note Block 在两种编辑器中的不同角色

BlockSuite 提供两种编辑器,Note Block 在其中的行为截然不同(参见 page-editor.md 与 edgeless-editor.md):

页面(Doc)编辑器中:唯一的正文容器

如果一份文档完全在页面编辑器中编辑,那么它的全部文本内容都会放置在一个 Note Block 中。此时 Note Block 承担的是传统富文本编辑器(如 ProseMirror、Slate)中"正文区"的角色——用户的输入、粘贴的内容、插入的图片/数据库等块,都作为 Note Block 的子块存在。

在页面编辑器中,Note Block 的显示顺序由它在根块(Page Block)children数组中的排列顺序决定。也就是说,它是纯文档流式的:先出现的 Note 排上面,后出现的排下面。

画布(Edgeless)编辑器中:可自由摆放与拆分的便签

在画布编辑器中,情况完全不同:

  • 画布允许放置多个 Note Block,每个便签都可以被拖动、缩放、设置背景色和阴影;
  • 画布还支持将一个 Note Block 的内容拆分成多个不同的 Note(例如通过工具栏或拖拽把一部分子块移出);
  • 每个 Note Block 的位置由xywh字段决定,与其它图形内容的层叠关系由index字段决定,从而可以和 shape、connector、frame 等图形元素一起被自由定位在无限画布上。

值得强调的是,BlockSuite 的两种编辑器可以绑定同一个 doc 对象(见 page-editor.md 的 "runtime compatibility" 说明),因此同一份数据既能以流式 Note 呈现,也能以画布便签呈现。

Schema 与模型层:源码级的定位与属性定义

Note Block 的 Schema 定义在 note-model.ts,flavour 为affine:note。通过defineBlockSchema声明了默认属性与块元数据:

export const NoteBlockSchema = defineBlockSchema({ flavour: 'affine:note', props: (): NoteProps => ({ xywh: `[0,0,${NOTE_WIDTH},95]`, // 默认宽 800,高 95 background: DEFAULT_NOTE_BACKGROUND_COLOR, // 默认蓝色背景 index: 'a0', hidden: false, // 已废弃,改用 displayMode displayMode: NoteDisplayMode.DocAndEdgeless, edgeless: { style: { borderRadius: 0, borderSize: 4, borderStyle: StrokeStyle.None, shadowType: DEFAULT_NOTE_SHADOW, }, }, }), metadata: { version: 1, role: 'hub', parent: ['affine:page'], children: [ 'affine:paragraph', 'affine:list', 'affine:code', 'affine:divider', 'affine:database', 'affine:data-view', 'affine:image', 'affine:bookmark', 'affine:attachment', 'affine:surface-ref', 'affine:embed-*', ], }, toModel: () => new NoteBlockModel(), });

这里的几个关键点:

  • role: 'hub':表明 Note Block 是一个"枢纽型"容器,负责组织其下所有内容块,这与文档中"container block"的定位完全对应。
  • parent: ['affine:page']:Note Block 只能作为 Page Block 的直接子节点。
  • children列表:Note Block 允许承载段落、列表、代码、分割线、数据库、数据视图、图片、书签、附件、surface-ref 以及所有affine:embed-*嵌入类块——这解释了为什么它是"所有文本内容的唯一宿主"。

xywh字段:画布上的几何定位

xywhSerializedXYWH类型(形如[x,y,w,h]的序列化字符串)。默认值为[0,0,800,95],其中NOTE_WIDTH = 800定义在 consts/note.ts。在画布编辑器中:

  • xy表示 Note 左上角在画布坐标系中的位置;
  • wh表示便签的宽高;
  • 组件通过Bound.deserialize(this.model.xywh)将其解析为几何矩形,用于渲染、命中测试和拖拽。

从源码结构看,Note Block 的模型类NoteBlockModel继承自GfxCompatible(BlockModel)并实现GfxElementGeometry接口(见 note-model.ts),因此它天然拥有containsBoundincludesPointintersectsBound等几何能力,可以被画布选择框、套索、吸附等图形系统识别。注意,当displayMode === NoteDisplayMode.DocOnly时,_isSelectable()返回 false,画布上的几何命中会被禁用。

index字段:画布上的层叠顺序

index是 Gfx 层(layer)系统中用于确定元素 z 轴顺序的字符串键,默认值为'a0'。在画布编辑器中,Note Block 与 shape、connector 等图形元素统一参与index排序,渲染时通过this.rootService.layer.getZIndex(this.model)计算 z-index(见 note-edgeless-block.ts)。正因如此,便签才能和图形内容互相覆盖、自由叠放。

显示模式:displayMode 与 hidden

displayMode是 Note Block 最重要的显示控制属性,类型为NoteDisplayMode枚举,定义在 consts/note.ts:

枚举值字符串值含义
DocAndEdgeless'both'在页面编辑器和画布编辑器中都显示(默认值)
DocOnly'doc'仅在页面编辑器中显示
EdgelessOnly'edgeless'仅在画布编辑器中显示

模型源码中标注了hidden属性已废弃,并给出了迁移映射:

  • hidden: truedisplayMode: NoteDisplayMode.EdgelessOnly(仅在画布模式可见);
  • hidden: falsedisplayMode: NoteDisplayMode.DocAndEdgeless(两种模式均可见)。

这一设计允许同一份文档在"纯文档视图"和"画布视图"下展示不同的 Note 集合,例如在画布上补充批注便签,而页面视图中不显示它。EdgelessNoteBlockComponent的渲染逻辑也据此分支:当displayMode === NoteDisplayMode.DocOnly时直接返回空(见 note-edgeless-block.ts)。

画布便签的视觉样式属性

edgeless.style对象控制便签在画布上的外观(默认值见 note-model.ts):

  • borderRadius:圆角半径(默认 0);
  • borderSize:边框粗细(默认 4);
  • borderStyle:边框样式,StrokeStyle枚举取值dash/none/solid(默认none,即无边框);
  • shadowType:阴影类型,可选值定义在 consts/note.ts:空字符串(无阴影)、--affine-note-shadow-box--affine-note-shadow-sticker(默认)、--affine-note-shadow-paper--affine-note-shadow-float--affine-note-shadow-film

此外还有collapsecollapsedHeightscale三个扩展属性:scale控制便签内容的缩放;collapsecollapsedHeight用于"折叠便签"——折叠后只显示固定高度,悬停或拖拽时通过底部折叠按钮展开(见 note-edgeless-block.ts)。背景色由background属性控制,默认值NOTE_BACKGROUND_COLORS[5]即蓝色,完整的 11 种色板同样定义在 consts/note.ts。

组件与服务:双视图与统一服务

两种视图组件

BlockSpec 中为 Note Block 注册了两个视图组件(见 note-spec.ts):

  • NoteBlockSpec:页面编辑器视图,渲染affine-note自定义元素,即 note-block.ts 中的NoteBlockComponent。它的渲染逻辑非常直接:一个flow-root容器内部调用this.renderChildren(this.model)渲染所有子块,选中时叠加--affine-hover-color背景;
  • EdgelessNoteBlockSpec:画布编辑器视图,渲染affine-edgeless-note,即 note-edgeless-block.ts 中的EdgelessNoteBlockComponent。它通过toGfxBlockComponent(NoteBlockComponent)复用页面版组件,并叠加画布能力:背景层(note-background)、内容裁剪容器(overflow-y: clip)、折叠按钮、edgeless-note-mask交互遮罩等。

画布版组件还会自动根据内容高度同步xywh:通过ResizeObserver监听内容尺寸变化,将bound.h更新为实际内容高度(见 note-edgeless-block.ts)。也就是说,在画布中Note 的默认高度是自适应内容而非固定 95。同时,点击便签空白区域时,组件会就近计算插入位置并自动addSiblingBlocks补一个段落块,保证点击处可以直接开始输入(见 note-edgeless-block.ts)。

NoteBlockService:命令与拖拽能力

NoteBlockService(note-service.ts)继承BlockService<NoteBlockModel>,在mounted()时注册两类能力:

  1. 快捷键:绑定Tab/Shift-Tab分别执行indentBlocks/dedentBlocks命令,用于调整光标所在块的缩进层级;
  2. 拖拽手柄(Drag Handle)选项:当从 Note 的拖拽手柄开始拖拽时,生成便签的实时预览(renderModel渲染 +Bound偏移计算),支持把整个 Note 拖出画布;拖拽结束落点不在 Note 内部时——若按住Alt复制Note 的全部子块插入目标位置,否则移动子块到目标块并删除原 Note(见 note-service.ts)。这正是"将一个 Note 拆分成多个 Note"的底层实现路径之一。

KeymapController:块级键盘交互

页面版组件在connectedCallback中绑定 keymap-controller.ts(见 note-block.ts),它基于 BlockSuite 的命令链系统处理 Note 内部的键盘事件:

  • ArrowDown / ArrowUp:在文本选区与块选区之间智能导航。若下一个块是 paragraph/list/code 则沿用默认文本行为,否则切换为块选区(selectBlock);
  • Shift-ArrowDown / Shift-ArrowUp:基于锚点块(anchor block)连续多选块(selectBlocksBetween),并保证焦点块始终停留在当前 Note 容器内;
  • Enter:在块选区下于当前块后插入新的 paragraph 并聚焦;
  • Escape:清除块选区,回到文本编辑状态;
  • Mod-a:在 Note 内部全选所有子块;
  • 另外还批量注册了移动块、快速操作(quick action)与文本类型转换(如段落转代码块)等配置热键。

从实现上看,KeymapController把"Note 内块导航"这一复杂交互完全收敛到容器组件内,用户无需关心块级边界,体验上接近单一文档流。

如何在实际项目中使用

由于 Note Block 是页面编辑器的默认正文容器,在常规使用中你几乎不需要手动创建它——初始化 doc 并绑定 PageEditor 后,正文会自动落在 Note Block 中。以下场景需要你主动关注它:

  • 构建纯页面编辑器:使用NoteBlockSpec,文档只有一个 Note,全部内容按文档流排版;
  • 构建白板应用:使用EdgelessNoteBlockSpec,通过修改模型的xywh摆放多个便签,用index控制层叠,用displayMode控制便签是否在页面视图中出现;
  • 拆分便签:借助 Note 的拖拽手柄或编程方式(doc.moveBlocks/doc.addBlocks),将一个 Note 的子块迁移到另一个 Note。

如果你想在自己的 BlockSuite 应用中定制 Note Block 行为,可以从这几处入手:note-spec.ts(注册视图组件)、note-model.ts(默认属性与子块白名单)、note-service.ts(服务与拖拽选项)、note-edgeless-block.ts(画布渲染细节)。

参考

  • 官方文档:note-block.md、page-editor.md、edgeless-editor.md
  • Schema 与模型:note-model.ts、note.ts
  • 组件与服务:note-block.ts、note-edgeless-block.ts、note-service.ts、keymap-controller.ts、note-spec.ts
  • 嵌套结构示意图:block-nesting.png

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询