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字段:画布上的几何定位
xywh是SerializedXYWH类型(形如[x,y,w,h]的序列化字符串)。默认值为[0,0,800,95],其中NOTE_WIDTH = 800定义在 consts/note.ts。在画布编辑器中:
x、y表示 Note 左上角在画布坐标系中的位置;w、h表示便签的宽高;- 组件通过
Bound.deserialize(this.model.xywh)将其解析为几何矩形,用于渲染、命中测试和拖拽。
从源码结构看,Note Block 的模型类NoteBlockModel继承自GfxCompatible(BlockModel)并实现GfxElementGeometry接口(见 note-model.ts),因此它天然拥有containsBound、includesPoint、intersectsBound等几何能力,可以被画布选择框、套索、吸附等图形系统识别。注意,当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: true→displayMode: NoteDisplayMode.EdgelessOnly(仅在画布模式可见);hidden: false→displayMode: 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。
此外还有collapse、collapsedHeight、scale三个扩展属性:scale控制便签内容的缩放;collapse与collapsedHeight用于"折叠便签"——折叠后只显示固定高度,悬停或拖拽时通过底部折叠按钮展开(见 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()时注册两类能力:
- 快捷键:绑定
Tab/Shift-Tab分别执行indentBlocks/dedentBlocks命令,用于调整光标所在块的缩进层级; - 拖拽手柄(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),仅供参考