@blocksuite/editor 包含了内置在AFFiNE中的编辑器。其nightly版本每天基于主要分支发布,且它们总是在CI上进行测试。这意味着nightly版本可以随时在真实项目中像AFFiNE一样使用:
pnpm i @blocksuite/editor@nightly
如果您想要轻松重用大多数富文本编辑功能,可以直接使用 SimpleAffineEditor Web组件(代码示例在这里): import { SimpleAffineEditor } from '@blocksuite/editor';
import '@blocksuite/editor/themes/affine.css';
const editor = new SimpleAffineEditor();
document.body.appendChild(editor);
或者等效地,您也可以使用声明式样式:
<body>
<simple-affine-editor></simple-affine-editor>
<script type="module">
import '@blocksuite/editor';
import '@blocksuite/editor/themes/affine.css';
</script>
</body>
然而,这里的SimpleAffineEditor只是一个几十行的薄包装器,它不支持选择加入协作和数据持久化功能。如果您要支持更复杂的真实世界用例(例如,使用自定义块模型和配置数据源),这将涉及使用以下三个核心包中的这些功能:
pnpm i \\
@blocksuite/store@nightly \\
@blocksuite/blocks@nightly \\
@blocksuite/editor@nightly
在接下来的章节中,我们将继续演示它们的用法和涉及的核心概念。
BlockSuite 的核心是块的概念。然而,为了高效地处理大量的块,这些块被组织成工作区和页面。工作区 是最高级别的容器,可以容纳多个页面。页面 是组织块的子容器,每个页面包含一个强类型的块树。
在 BlockSuite 中,工作区 作为组织页面的顶级容器。创建工作区后,用户可以将不同集合的页面分组和分类,每个页面表示一个特定的项目或相关内容的集合。以下是创建新工作区的示例:
import { Workspace } from '@blocksuite/store';
const workspace = new Workspace({ id: 'foo' });
// 我们可以将块的批量注册到工作区中
workspace.register(AffineSchemas);
在 BlockSuite 中,页面 是实际组织块的容器,允许用户通过其 API 操作块树。在涉及富文本文档的典型场景中,一个文档由工作区内的单个页面表示。
以下是在工作区内创建 id 为 page0 的新页面的示例:
const page = workspace.createPage({ id: 'page0' });
page 实例提供了一组核心 API 来执行块操作,例如 page.addBlock、page.updateBlock 和 page.deleteBlock。这些 API 将在本文档的以下章节中进一步介绍。
在BlockSuite中,块是结构化内容的基本单元,代表文本、图像或其他媒体元素,甚至是嵌套的子文档。BlockSuite支持定义各种类型的块,称为口味块。通过组合和嵌套块,用户可以创建丰富的结构化内容。
块的术语“口味”受物理学概念启发,其值遵循namespace:name格式。例如,我们允许affine:paragraph块具有类似的子类型,例如h1、h2、h3、quote等,这减少了冗余代码,并使具有相似行为的块之间更容易相互转换。 💡
通常,“块口味”和“块类型”这两个术语可以互换使用。(In general, the terms "block flavour" and "block type" can be used interchangeably.)
在BlockSuite中,用于管理块的基本API包括addBlock、updateBlock和deleteBlock。这些操作允许用户在“页面”内创建、修改和删除块,提供了一种管理和组织结构化内容的简单方法。
page.addBlock方法需要一个必需的flavour参数。此参数标记要添加的块类型。可选地,您可以使用包含新块属性的props参数,和指定父块和应插入新块的索引的parent和parentIndex参数。
使用示例:
import { Text } from '@blocksuite/store';
const props = { title: new Text('My New Page') };
const newBlockId = page.addBlock('affine:page', props);
page.addBlock返回添加块的自动生成的id,而不是块实例。块实例将同步添加到页面,并且可以通过调用page.getBlockById(id)检索。要访问页面块树上的任何块,只需使用page.root.children[0].children[1]引用即可。
页面上的每个块实例都是一个简单的JavaScript模型,表示块树上的节点。每个块节点至少包含三个字段:
请注意,段落块也可以使用children字段嵌套另一个段落块而没有中间级别。
信息
返回**id**而不是块实例的原因是为了使API默认协作(文档WIP)。
page.updateBlock方法用于修改现有块的属性。它需要两个参数:要更新的块实例和包含更新属性的对象。
使用示例:
const props = { text: new Text('New paragraph') };page.updateBlock(block, props);
同样,您可以使用page.deleteBlock方法从块树中删除块。
affine:page块用作页面的顶层容器,在页面层次结构中包含所有其他块。
affine:frame块在页面内作为平面容器,可以在白板上定位多个帧。
每个affine:paragraph块在页面内包含一个富文本的线性序列。它使用户能够创建基于文本的内容并根据自己的需求进行样式设置。
提示
在AFFiNE中,affine:frame块对于将文档模式与白板模式混合是至关重要的。在文档模式下,帧作为不影响其内部块的显示的透明容器。但是,在白板模式下,帧具有绝对定位,使其内容可以自由放置在画布上。
在预构建的编辑器中还有更多的块口味,包括:
在BlockSuite中,块还可以分为两种不同的角色:
HubBlock作为空块容器,其中包括affine:page、affine:frame和affine:database。
ContentBlock包含原子内容,包括affine:paragraph、affine:list、affine:code和affine:embed。
HubBlock作为容器,会影响其包含的块的呈现方式。例如,帧块可以在白板上绝对定位,而数据库块可以将其每个子块显示为单独的行或将其进一步分组为板。相反,ContentBlock只能嵌套其他ContentBlock以表达嵌套的Markdown列表等结构。
💡
This section is subject to change in future updates.要定义新的块,您需要定义并注册其模式,该模式描述了块的形状。可以使用defineBlockSchemaAPI声明性地完成此操作:
import { defineBlockSchema } from '@blocksuite/store';
type ListType = 'bulleted' | 'numbered' | 'todo';
const ListBlockSchema = defineBlockSchema({
flavour: 'affine:list',
// The `Text` here is used for defining built-in rich text type
props: ({ Text }) => ({
// Supports strongly typed enums
type: 'bulleted' as ListType,
// Rich text content that supports inline formats
text: Text(),
// A boolean property with default value
checked: false,
}),
metadata: {
// Used for data validation and migration
version: 1,
// 'content' | 'hub'
role: 'content',
},
});
在此示例中,props定义每个块实例上的字段,而metadata包含特定于此块口味的单例元数据。注册块口味到工作区后,您可以使用pageAPI对其进行操作:
workspace.register([ListBlockSchema]);
const page = workspace.createPage();
const id = page.addBlock('affine:list');
const listBlock = page.getBlockById(id);
// Convert the list type
page.updateBlock(listBlock, { type: 'numbered' });
到目前为止,我们只涵盖了使用框架无关的@blocksuite/store包定义块模型,接下来的章节中,我们将解释如何将块模型集成到UI框架中,以及如何在AFFiNE中构建自己的可编辑块,其中涉及其他运行时概念。