news 2026/9/17 2:30:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/blocksuite

Block Spec 是 BlockSuite 中定义"一种块类型"的元信息结构,它将数据(schema)、行为(service)与渲染(view)三者封装为一个可插拔单元。本指南以官方文档 block-spec.md 为主线,结合 block-std 与 blocks 的真实源码,帮助你完整理解 BlockSpec 的每个字段,并学会用@blocksuite/lit编写属于自己的块。

BlockSpec 将 schema、service、view(component + widgets)组织为一个整体,是编辑器按类型装配块能力的核心载体。

BlockSpec 是什么

在 BlockSuite 中,BlockSpec定义了一种特定块类型在编辑器内的结构与交互元素。BlockSuite 编辑器本质上是由若干 block spec 组装而成的:编辑器的顶层 UI 通常也被实现为一个专用块,一般是affine:page类型的根块(root block)。

换句话说,当你使用EditorContainer或自定义的编辑器宿主时,传入的specs数组就是整个编辑器的"能力清单"——清单里有哪些 spec,编辑器就支持哪些块、哪些命令、哪些服务。

一个 block spec 包含以下核心属性:

属性作用对应文档
schema定义块内容的结构与数据类型block-schema.md
service注册特定动作与外部调用的方法block-service.md
view表示块的视觉呈现与布局block-view.md
view.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: BlockView<WidgetNames>; config?: BlockConfig; commands?: BlockCommands; service?: BlockServiceConstructor<Service>; setup?: (slots: BlockSpecSlots, disposableGroup: DisposableGroup) => void; }

可以推断,除文档重点讲解的schemaserviceview之外,接口还预留了:

  • config:块的运行时配置对象,可通过SpecStore.getConfig(flavour)读取(见 spec-store.ts);
  • commands:该块注册到std.command命令系统的命令集合;
  • setup:spec 挂载阶段执行的初始化钩子,可订阅BlockSpecSlots中的生命周期事件。

view的类型定义(同文件 type.ts)也印证了文档中的描述:

export interface BlockView<WidgetNames extends string = string> { component: StaticValue | ((model: BlockModel) => StaticValue); widgets?: Record<WidgetNames, StaticValue>; }

component既可以直接是一个 lit 的StaticValue,也可以是一个根据BlockModel动态返回StaticValue的函数;widgets则是"widget 名 → lit 静态模板"的映射表。

一个完整的 Lit 版 BlockSpec 示例

官方文档强调:在 block spec 中,view的定义与 UI 框架相关。默认情况下,项目提供了@blocksuite/lit包来帮助构建基于 Lit 的块视图,但依然可以使用其他 UI 框架(后续文档会介绍如何编写自定义块渲染器)。

下面是一个基于 Lit 的 block spec 示例(摘自 block-spec.md):

import type { BlockSpec } from '@blocksuite/block-std'; import { literal } from 'lit/static-html.js'; const MyBlockSepc: BlockSpec = { schema: MyBlockSchema, service: MyBlockService, view: { component: literal`my-block-component`, widgets: { myBlockToolbar: literal`my-block-toolbar`, myBlockMenu: literal`my-block-menu`, }, }, };

注意这里使用的是lit/static-html.jsliteral标签,它生成的是静态 HTML 模板而非真实 DOM。因为块组件是延迟到渲染时才按需实例化的自定义元素,literal的"模板即标记"特性可以保证在不同块之间复用同一模板实例,从而避免重复创建。

仓库中的真实示例

这一写法并非虚构,仓库中大量内置块就是这样定义的。例如 paragraph-spec.ts:

export const ParagraphBlockSpec: BlockSpec = { schema: ParagraphBlockSchema, view: { component: literal`affine-paragraph`, }, commands, service: ParagraphBlockService, };

而 note-spec.ts 展示了更有意思的用法:同一个NoteBlockSchemaNoteBlockService,通过更换view.component就能在 page 模式和 edgeless 模式下呈现完全不同的渲染组件:

export const NoteBlockSpec: BlockSpec = { schema: NoteBlockSchema, service: NoteBlockService, view: { component: literal`affine-note`, }, commands, }; export const EdgelessNoteBlockSpec: BlockSpec = { schema: NoteBlockSchema, service: NoteBlockService, view: { component: literal`affine-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:paragraphaffine:note);
  • props是块拥有的属性,可以被用户操作更新,也可以用来渲染块。典型 props 有textlevelurlsrc等;
  • props 中可以使用大多数原始类型,但不应该使用undefinednull
  • props 还支持一类特殊类型,称为internal类型,用于描述块的内部数据结构;
  • internal.Text是表示块文本的特殊类型,它对应 Yjs 中的 Y.Text——这正体现了 BlockSuite 的 CRDT 原生数据流设计;
  • props 中也允许使用数组和对象。

schema 关系(relations)

你可以在 schema 中声明块与块之间的关系。

role:三种角色

每个块都必须声明role,取值有三种:

  • root:文档的根块。一个文档只能有一个 root 块;
  • hub:集散块(hub),可以拥有多个子块,子块可以是hub也可以是content
  • content:文档的叶子块。content 块只能有一个父块,且只能以content作为自己的子块。

例如:

root || hub1 || | content1 || | | content2 || hub2 || | hub3 || | | content3 || | content4
parent 与 children 约束

默认情况下,块会根据role校验子块与父块。你可以在 schema 的metadata中传入parentchildren选项覆盖默认行为。以下示例来自 block-schema.md:

含义是"该块的子块必须匹配 flavourmy-leaf":

import { 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 到 Model

schema 用于生成块的 model。默认情况下,model 持有块的 flavour、props 和 id:

MyBlockSchema -> MyBlockModel-1 -> MyBlockModel-2 -> MyBlockModel-3

例如,定义如下 schema:

import { 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: SchemaToModel<typeof MyBlockSchema>) { const id = model.id; const flavour = model.flavour; const text = model.text; const level = model.level; }

你也可以继承BlockModel定制 model,为其补充更多方法:

export class MyBlockModel extends BlockModel<MyBlockProps> { 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 = SchemaToModel<typeof myBlockSchema>; class MyBlockService extends BlockService<MyBlockModel> { //... }

每种块类型的 service 只会被实例化一次,而且即使当前文档中没有任何该块的实例,service 也会被实例化。因此它被设计为"面向某一类块的编辑器级方法"的载体。

例如,在 service 中绑定创建新块的快捷键:

class MyBlockService extends BlockService<MyBlockModel> { override mounted() { super.mounted(); this.bindHotkey( { 'Alt-1': this._addMyBlock, }, { global: true } ); } private _addMyBlock = () => { this.doc.addBlock('my-block', {}); }; }

生命周期钩子

BlockService类提供以下生命周期钩子供你覆写:

  • mounted:service 实例化时调用;
  • unmounted:service 被销毁时调用。

这两个钩子与 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 = SchemaToModel<typeof myBlockSchema>; @customElements('my-block') class MyBlockView extends BlockComponent<MyBlockModel> { override render() { return html` <div> <h3>My Block</h3> </div> `; } }

注意:这里声明的自定义元素名my-block,必须与 block spec 中view.componentliteral\my-block-component`对应——literal模板实际上就是告诉渲染系统"去实例化名为my-block-component` 的自定义元素"。

渲染子块

块可以有子块,通过renderModelChildren渲染:

@customElements('my-block') class MyBlockView extends BlockComponent<MyBlockModel> { override render() { return html` <div> <h3>My Block</h3> ${this.renderModelChildren(this.model)} </div> `; } }

读取与更新 props

在块视图中可以方便地读取和更新 props。通过this.doc.updateBlock更新 model 属性:

@customElements('my-block') class MyBlockView extends BlockComponent<MyBlockModel> { private _onClick = () => { this.doc.updateBlock(this.model, { count: this.model.count + 1, }); }; override render() { return html` <div> <h3>My Block</h3> <p>Count: ${this.model.count}</p> <button @click=${this._onClick}>Add</button> </div> `; } }

也可以监听 props 变化来创建类似"计算属性"的效果。监听this.model.propsUpdated事件:

@customElements('my-block') class MyBlockView extends BlockComponent<MyBlockModel> { private _yen = '0¥'; override connectedCallback() { super.connectedCallback(); this.model.propsUpdated.on(() => { this._yen = `${this.model.count * 100}¥`; }); } override render() { return html` <div> <h3>My Block</h3> <p>Price: ${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 WidgetComponent<MyBlockView> { override render() { return html` <div> <h3>My Widget</h3> </div> `; } }

获取宿主块(host block)

widget 总是与一个称为宿主块(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 WidgetComponent<CodeBlockComponent> { private _onChange = e => { this.doc.updateBlock(this.blockComponent.model, { language: e.target.value, }); }; override render() { return html` <select @change=${this._onChange}> <option value="javascript">JavaScript</option> <option value="python">Python</option> </select> `; } }

同样地,在 widget 中也能通过this.std获取std实例。widget 会作为view.widgets映射中的一个条目被注册进 block spec,例如示例中的myBlockToolbarmyBlockMenu

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-L93);
  • getService(flavour):按 flavour 取 service,即上文图片代理示例的底层实现;
  • getView(flavour)getConfig(flavour):分别取块的 view 与 config;
  • mount()/unmount():编辑器挂载/卸载时统一调度所有 service 的生命周期。

生命周期事件本身定义在 slots.ts:mountedunmountedviewConnectedviewDisconnectedwidgetConnectedwidgetDisconnected六个 Slot,供setup钩子订阅。

仓库中还提供了SpecBuilder(见 specs/utils/spec-builder.ts),它可以在不修改原 spec 对象的前提下,按 flavour 追加setup逻辑——典型的"在不动内置块实现的情况下扩展编辑器能力"的入口。

继续深入

block spec 的每个组成部分都有对应的专题文档,建议按以下顺序继续阅读:

  • block-schema.md:defineBlockSchema、flavour/props、role 与父子关系、SchemaToModel的完整讲解;
  • block-service.md:BlockService生命周期与运行时配置;
  • block-view.md:BlockComponent、子块渲染与 props 读写;
  • block-widgets.md:WidgetComponent与宿主块交互;
  • 若想从整体理解编辑器的组装方式,可阅读 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 2:30:07

Qt+FFmpeg硬解播放器:多路RTSP+OpenGL渲染实战框架

简介&#xff1a;这是一款基于Qt 5.8开发的高性能音视频播放器工程&#xff0c;面向音视频开发初学者与嵌入式/桌面端多媒体应用开发者&#xff0c;解决多路实时流与本地文件混合播放、软硬解码协同、GPU加速渲染等典型工程难题。资源包共1090个文件&#xff0c;含785个头文件&…

作者头像 李华
网站建设 2026/9/17 2:28:28

基于浮动车GPS轨迹反推交通信号灯周期与配时参数

简介&#xff1a;本资源为2024年华中杯数学建模竞赛B题完整参赛成果包&#xff0c;面向计算机、人工智能、交通工程、自动化等专业本科生及建模初学者&#xff0c;聚焦“基于行车轨迹数据反推交通信号灯周期”这一典型城市交通感知问题。资源包含可直接运行的MATLAB源代码&…

作者头像 李华
网站建设 2026/9/17 2:26:51

前馈控制原理与WPO工程实践:从work4.zip到嵌入式部署

简介&#xff1a;本资源是一份面向自动化与控制工程专业学生、初阶科研人员及MATLAB实践者的前馈控制系统仿真学习材料&#xff0c;聚焦于前馈控制原理建模、扰动补偿设计与Simulink/编程联合实现。资源核心为1个可直接运行的MATLAB脚本文件&#xff08;work4.m&#xff09;&am…

作者头像 李华
网站建设 2026/9/17 2:25:13

示波器八大核心问题:触发、探头、采样率与噪声的工程真相

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华