ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

BlockSuite Block Spec 完全指南:用 schema、service、view 组装可插拔的编辑器块

2026/9/17 2:30:16 拓冰建站 浏览量
BlockSuite Block Spec 完全指南:用 schema、service、view 组装可插拔的编辑器块 BlockSuite Block Spec 完全指南用 schema、service、view 组装可插拔的编辑器块【免费下载链接】blocksuite Content editing tech stack for the web - BlockSuite is a toolkit for building editors and collaborative applications.项目地址: https://gitcode.com/GitHub_Trending/bl/blocksuiteBlock Spec 是 BlockSuite 中定义一种块类型的元信息结构它将数据schema、行为service与渲染view三者封装为一个可插拔单元。本指南以官方文档 block-spec.md 为主线结合 block-std 与 blocks 的真实源码帮助你完整理解 BlockSpec 的每个字段并学会用blocksuite/lit编写属于自己的块。BlockSpec 将 schema、service、viewcomponent widgets组织为一个整体是编辑器按类型装配块能力的核心载体。BlockSpec 是什么在 BlockSuite 中BlockSpec定义了一种特定块类型在编辑器内的结构与交互元素。BlockSuite 编辑器本质上是由若干 block spec 组装而成的编辑器的顶层 UI 通常也被实现为一个专用块一般是affine:page类型的根块root block。换句话说当你使用EditorContainer或自定义的编辑器宿主时传入的specs数组就是整个编辑器的能力清单——清单里有哪些 spec编辑器就支持哪些块、哪些命令、哪些服务。一个 block spec 包含以下核心属性属性作用对应文档schema定义块内容的结构与数据类型block-schema.mdservice注册特定动作与外部调用的方法block-service.mdview表示块的视觉呈现与布局block-view.mdview.component块的主要 UI 元素同上view.widgets增强块功能的附加交互元素block-widgets.md从源码看 BlockSpec 的完整形状官方文档描述的是最核心的三个字段而实际类型定义比文档更丰富。查看 packages/framework/block-std/src/spec/type.ts可以看到BlockSpec接口的完整定义export interface BlockSpec WidgetNames extends string string, BlockConfig object, Service extends BlockService BlockService, { schema: BlockSchemaType; view: BlockViewWidgetNames; config?: BlockConfig; commands?: BlockCommands; service?: BlockServiceConstructorService; setup?: (slots: BlockSpecSlots, disposableGroup: DisposableGroup) void; }可以推断除文档重点讲解的schema、service、view之外接口还预留了config块的运行时配置对象可通过SpecStore.getConfig(flavour)读取见 spec-store.tscommands该块注册到std.command命令系统的命令集合setupspec 挂载阶段执行的初始化钩子可订阅BlockSpecSlots中的生命周期事件。而view的类型定义同文件 type.ts也印证了文档中的描述export interface BlockViewWidgetNames extends string string { component: StaticValue | ((model: BlockModel) StaticValue); widgets?: RecordWidgetNames, StaticValue; }component既可以直接是一个 lit 的StaticValue也可以是一个根据BlockModel动态返回StaticValue的函数widgets则是widget 名 → lit 静态模板的映射表。一个完整的 Lit 版 BlockSpec 示例官方文档强调在 block spec 中view的定义与 UI 框架相关。默认情况下项目提供了blocksuite/lit包来帮助构建基于 Lit 的块视图但依然可以使用其他 UI 框架后续文档会介绍如何编写自定义块渲染器。下面是一个基于 Lit 的 block spec 示例摘自 block-spec.mdimport type { BlockSpec } from blocksuite/block-std; import { literal } from lit/static-html.js; const MyBlockSepc: BlockSpec { schema: MyBlockSchema, service: MyBlockService, view: { component: literalmy-block-component, widgets: { myBlockToolbar: literalmy-block-toolbar, myBlockMenu: literalmy-block-menu, }, }, };注意这里使用的是lit/static-html.js的literal标签它生成的是静态 HTML 模板而非真实 DOM。因为块组件是延迟到渲染时才按需实例化的自定义元素literal的模板即标记特性可以保证在不同块之间复用同一模板实例从而避免重复创建。仓库中的真实示例这一写法并非虚构仓库中大量内置块就是这样定义的。例如 paragraph-spec.tsexport const ParagraphBlockSpec: BlockSpec { schema: ParagraphBlockSchema, view: { component: literalaffine-paragraph, }, commands, service: ParagraphBlockService, };而 note-spec.ts 展示了更有意思的用法同一个NoteBlockSchema与NoteBlockService通过更换view.component就能在 page 模式和 edgeless 模式下呈现完全不同的渲染组件export const NoteBlockSpec: BlockSpec { schema: NoteBlockSchema, service: NoteBlockService, view: { component: literalaffine-note, }, commands, }; export const EdgelessNoteBlockSpec: BlockSpec { schema: NoteBlockSchema, service: NoteBlockService, view: { component: literalaffine-edgeless-note, }, commands, };这说明view是 block spec 中与 UI 框架耦合最紧、也最灵活的一环。schema定义块的数据结构所有块都必须有 schema它描述块的数据结构。使用blocksuite/store导出的defineBlockSchema函数来定义import { defineBlockSchema } from blocksuite/store; export const MyBlockSchema defineBlockSchema({ flavour: my-block, props: internal ({ text: internal.Text(), level: 0, }), metadata: { version: 1, role: content, }, });flavour 与 props 要点官方文档给出了以下几个关键结论flavour是块的唯一标识字符串可以把它理解为块的名字例如内置块的affine:paragraph、affine:noteprops是块拥有的属性可以被用户操作更新也可以用来渲染块。典型 props 有text、level、url、src等props 中可以使用大多数原始类型但不应该使用undefined或nullprops 还支持一类特殊类型称为internal类型用于描述块的内部数据结构internal.Text是表示块文本的特殊类型它对应 Yjs 中的 Y.Text——这正体现了 BlockSuite 的 CRDT 原生数据流设计props 中也允许使用数组和对象。schema 关系relations你可以在 schema 中声明块与块之间的关系。role三种角色每个块都必须声明role取值有三种root文档的根块。一个文档只能有一个 root 块hub集散块hub可以拥有多个子块子块可以是hub也可以是contentcontent文档的叶子块。content 块只能有一个父块且只能以content作为自己的子块。例如root || hub1 || | content1 || | | content2 || hub2 || | hub3 || | | content3 || | content4parent 与 children 约束默认情况下块会根据role校验子块与父块。你可以在 schema 的metadata中传入parent或children选项覆盖默认行为。以下示例来自 block-schema.md含义是该块的子块必须匹配 flavourmy-leafimport { defineBlockSchema } from blocksuite/store; export const MyBlockSchema defineBlockSchema({ // ... metadata: { children: [my-leaf], }, });传入*表示所有符合role规则的块都可以作为子块export const MyBlockSchema defineBlockSchema({ // ... metadata: { children: [*], }, });也支持 glob 模式export const MyBlockSchema defineBlockSchema({ // ... metadata: { children: [my-data-*], }, });glob 匹配能力由 minimatch 提供。传入空数组表示该块不接受任何子块export const MyBlockSchema defineBlockSchema({ // ... metadata: { children: [], }, });从 Schema 到 Modelschema 用于生成块的 model。默认情况下model 持有块的 flavour、props 和 idMyBlockSchema - MyBlockModel-1 - MyBlockModel-2 - MyBlockModel-3例如定义如下 schemaimport { defineBlockSchema, type Text } from blocksuite/store; export type MyBlockProps { text: Text; level: number; }; export const MyBlockSchema defineBlockSchema({ flavour: my-block, props: (internal): MyBlockProps ({ text: internal.Text(), level: 0, }), metadata: { version: 1, role: content, }, });当 model 创建后你可以通过SchemaToModel类型工具获得它的类型安全访问方式import { type SchemaToModel } from blocksuite/store; function doSomething(model: SchemaToModeltypeof MyBlockSchema) { const id model.id; const flavour model.flavour; const text model.text; const level model.level; }你也可以继承BlockModel定制 model为其补充更多方法export class MyBlockModel extends BlockModelMyBlockProps { levelUp() { this.level 1; } } function doSomething(model: MyBlockModel) { model.levelUp(); const level model.level; }service注册块级方法与生命周期每种块都可以注册自己的 service从而定义在编辑器生命周期中被调用的块专属方法。service 是一个继承自BlockService的类import { BlockService } from blocksuite/block-std; import { defineBlockSchema, type SchemaToModel } from blocksuite/store; const myBlockSchema defineBlockSchema({ //... }); type MyBlockModel SchemaToModeltypeof myBlockSchema; class MyBlockService extends BlockServiceMyBlockModel { //... }每种块类型的 service 只会被实例化一次而且即使当前文档中没有任何该块的实例service 也会被实例化。因此它被设计为面向某一类块的编辑器级方法的载体。例如在 service 中绑定创建新块的快捷键class MyBlockService extends BlockServiceMyBlockModel { override mounted() { super.mounted(); this.bindHotkey( { Alt-1: this._addMyBlock, }, { global: true } ); } private _addMyBlock () { this.doc.addBlock(my-block, {}); }; }生命周期钩子BlockService类提供以下生命周期钩子供你覆写mountedservice 实例化时调用unmountedservice 被销毁时调用。这两个钩子与 spec-store.ts 中的调度逻辑一一对应当新旧 spec 的 service 变化时旧 service 依次执行dispose()与unmounted()新 service 则被实例化并调用mounted()。设置运行时配置有时你需要为某些块设置运行时配置。典型例子是给 image 块设置图片代理中间件 URL默认情况下 image 块使用 AFFiNE 的图片代理来绕过 CORS 限制在自托管场景下默认代理不可用你可以设置自己的代理import type { ImageService } from blocksuite/blocks; const editorRoot document.querySelector(editor-host); if (!editorRoot) return; const imageService editorRoot.spec.getService(affine:image) as ImageService; // 调用具体方法设置运行时配置 imageService.setImageProxyURL(https://example.com/image-proxy);这里的spec.getService(flavour)正是SpecStore对外暴露的 service 访问入口见 spec-store.ts它按 flavour 从内部Map中取出已实例化的 service。不同块的运行时配置方法各不相同可以参考块的 API 文档找到你需要的方法。view块的可视化渲染在 BlockSuite 中块可以由任何 UI 框架渲染。一个块应渲染为一个 DOM 元素view就表示这个渲染器。默认提供基于 lit 的渲染器blocksuite/lit但也支持其他 UI 框架。基于 Web Component 的块视图项目提供BlockComponent类来帮助你构建基于 lit 的块视图import { defineBlockSchema, type SchemaToModel } from blocksuite/store; import { BlockComponent } from blocksuite/lit; import { html } from lit; import { customElement } from lit/decorators.js; const myBlockSchema defineBlockSchema({ //... props: () ({ count: 0, }), }); type MyBlockModel SchemaToModeltypeof myBlockSchema; customElements(my-block) class MyBlockView extends BlockComponentMyBlockModel { override render() { return html div h3My Block/h3 /div ; } }注意这里声明的自定义元素名my-block必须与 block spec 中view.component的literal\my-block-component对应——literal模板实际上就是告诉渲染系统去实例化名为my-block-component 的自定义元素。渲染子块块可以有子块通过renderModelChildren渲染customElements(my-block) class MyBlockView extends BlockComponentMyBlockModel { override render() { return html div h3My Block/h3 ${this.renderModelChildren(this.model)} /div ; } }读取与更新 props在块视图中可以方便地读取和更新 props。通过this.doc.updateBlock更新 model 属性customElements(my-block) class MyBlockView extends BlockComponentMyBlockModel { private _onClick () { this.doc.updateBlock(this.model, { count: this.model.count 1, }); }; override render() { return html div h3My Block/h3 pCount: ${this.model.count}/p button click${this._onClick}Add/button /div ; } }也可以监听 props 变化来创建类似计算属性的效果。监听this.model.propsUpdated事件customElements(my-block) class MyBlockView extends BlockComponentMyBlockModel { private _yen 0¥; override connectedCallback() { super.connectedCallback(); this.model.propsUpdated.on(() { this._yen ${this.model.count * 100}¥; }); } override render() { return html div h3My Block/h3 pPrice: ${this._yen}/p button click${this._onClick}Add/button /div ; } }在块组件内部你可以通过this.std拿到std实例从而使用block-std的全部能力命令系统、事件、selection 等。widgets块的附加交互组件widget 用于展示块的辅助 UI。有时你想为块显示一个提供额外信息或操作的菜单另一个常见实践是选中块时显示工具栏。widget 就是为此类功能设计的。与块类似widget 也依赖 UI 框架。默认使用blocksuite/lit提供基于 web component 的 widget 构建方案。Widget 组件使用WidgetComponent类构建基于 web component 的 widget 视图import { WidgetComponent } from blocksuite/lit; import { html } from lit; import { customElement } from lit/decorators.js; customElements(my-widget) class MyWidgetView extends WidgetComponentMyBlockView { override render() { return html div h3My Widget/h3 /div ; } }获取宿主块host blockwidget 总是与一个称为宿主块host block的块相关联可以通过BlockComponent类型的this.blockComponent属性获取它。例如对于一个展示代码示例的code block你想显示一个language pickerwidget 让用户切换代码语言可以这样定义import { WidgetComponent } from blocksuite/lit; import { html } from lit; import { customElement } from lit/decorators.js; customElements(my-widget) class CodeLanguagePicker extends WidgetComponentCodeBlockComponent { private _onChange e { this.doc.updateBlock(this.blockComponent.model, { language: e.target.value, }); }; override render() { return html select change${this._onChange} option valuejavascriptJavaScript/option option valuepythonPython/option /select ; } }同样地在 widget 中也能通过this.std获取std实例。widget 会作为view.widgets映射中的一个条目被注册进 block spec例如示例中的myBlockToolbar、myBlockMenu。Spec 的运行时管理SpecStore理解 block spec 如何被编辑器消费能让上面的知识串成一条线。核心实现在 packages/framework/block-std/src/spec/spec-store.ts要点如下_buildSpecMap将 spec 数组按spec.schema.model.flavour建立索引L26-L32_diffServices对比新旧 spec 集合service 不变的保留变化的先dispose()再重建并执行newSpec.setup?.(slots, ...)与service.mounted()L34-L71_registerCommands把每个 spec 的commands注册进std.command命令系统L73-L81这就是为什么 spec 可以声明命令applySpecs(specs)编辑器装配 block spec 列表的入口依次触发beforeApply/afterApply槽位L83-L93getService(flavour)按 flavour 取 service即上文图片代理示例的底层实现getView(flavour)、getConfig(flavour)分别取块的 view 与 configmount()/unmount()编辑器挂载/卸载时统一调度所有 service 的生命周期。生命周期事件本身定义在 slots.tsmounted、unmounted、viewConnected、viewDisconnected、widgetConnected、widgetDisconnected六个 Slot供setup钩子订阅。仓库中还提供了SpecBuilder见 specs/utils/spec-builder.ts它可以在不修改原 spec 对象的前提下按 flavour 追加setup逻辑——典型的在不动内置块实现的情况下扩展编辑器能力的入口。继续深入block spec 的每个组成部分都有对应的专题文档建议按以下顺序继续阅读block-schema.mddefineBlockSchema、flavour/props、role 与父子关系、SchemaToModel的完整讲解block-service.mdBlockService生命周期与运行时配置block-view.mdBlockComponent、子块渲染与 props 读写block-widgets.mdWidgetComponent与宿主块交互若想从整体理解编辑器的组装方式可阅读 overview.md 与 component-types.md内置块的 spec 定义分布在 packages/blocks/src 各块目录的*-spec.ts文件中如 paragraph-spec.ts、note-spec.ts是学习真实 spec 写法的最佳参考。掌握 BlockSpec就掌握了在 BlockSuite 中注册一种新块能力的全部入口用 schema 描述数据用 service 提供行为用 view 完成渲染用 widgets 增强交互——四者合一即可作为一块拼图接入任何 BlockSuite 编辑器。【免费下载链接】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),仅供参考