cover image
⛸️

Blocsuite 介绍

editornotion翻译

Monday, June 12, 2023

BlockSuite概述
BlockSuite(发音为“block sweet” 🍬)是一个综合项目,旨在提供一种用于构建协作应用程序的渐进式解决方案。它包括一个基于块的框架,用于组合丰富的内容编辑器,以及一个针对AFFiNE知识库量身定制的开箱即用的块编辑器。 BlockSuite背后的核心概念是块编辑,其中文本编辑和状态管理都以块为单位处理。这是通过使用CRDT技术实现分布式协作来实现的。此外,BlockSuite支持框架不可知的渲染,以确保可扩展性和灵活性。
基于块的编辑
在传统的富文本编辑器中,所有内容都存储在一个contenteditable DOM元素中。这种方法存在许多兼容性挑战,并且很难与现代UI框架集成。
BlockSuite引入了一种称为基于块的编辑的新范例。它的基于块的架构通过在其存储中管理块树来解决这些挑战。此树中的每个块节点都可以使用任何常见的UI框架进行渲染。如果多个块包含富文本,则它们将跨多个contenteditable实例进行呈现,从而提供更好的稳定性和兼容性。
例如,在基于BlockSuite的文档中包含两个段落、一张图片和一个包含10个文本行的表格中,您可以共存12个富文本编辑器实例。图像组件可以使用任何常见的UI框架进行实现,而无需放置在contenteditable容器中。这样可以实现各种组件之间的无缝集成和交互,同时在不同的UI框架之间保持兼容性和稳定性。
image
为了进一步增强基于块的编辑功能,BlockSuite还开发了自己的轻量级富文本编辑组件Virgo。它是一个Web组件,旨在像输入框一样为平面富文本内容提供功能,而不是一个沉重的富文本容器。这显著减少了冗余,允许通过嵌入Virgo和重用BlockSuite数据存储来实现稳定的富文本编辑能力。在实践中,您可以使用任何UI框架构建协作编辑器。
image
基于块的编辑使得可以使用流行的UI框架来呈现嵌套块内容,同时避免通常与contenteditable元素相关的不稳定性。
CRDT驱动的状态管理
BlockSuite的块编辑架构可能看起来很简单,但实际上,它需要先进的CRDT(不冲突复制数据类型)技术才能实现。这是因为在复杂的文档编辑场景中,状态变化通常跨越多个块。协调众多富文本实例之间的状态变化可能会很具有挑战性,特别是在追溯历史状态时。
然而,CRDT非常适合解决这些问题。BlockSuite的块树建立在高性能的Yjs库之上,该库自动记录和跟踪数据上的所有历史操作(包括每个字符),同时提供类似于MapArray的用户友好API。由于CRDT是其单一的真相来源,因此BlockSuite的块更新API不仅易于使用,而且形成了协作设计的单向数据流。
image
此外,由于CRDT能够将任何本地状态更新序列化为增量和标准化的数据结构,因此可以实现以下好处:
完全增量的撤消和重做,运行时实现零成本的时间旅行。
增量分布式更新数据,更新可以通过可插拔的提供者在不同的网络协议层上分发。
序列化整个应用程序状态为标准化数据格式,允许在浏览器外使用兼容的Rust实现进行读写。
BlockSuite中基于CRDT的状态管理能力是构建本地优先应用程序的先决条件,这将显着提高开发人员和用户的体验。为了支持AFFiNE项目,BlockSuite还开发了伴随的OctoBase项目,提供底层可插拔的持久性和数据同步支持。
通过利用CRDT驱动的状态管理,BlockSuite为处理复杂的结构内容更新提供了强大的解决方案。
框架无关的渲染
基于CRDT本地化架构设计,BlockSuite可以有效地将数据层与UI层分离。这使得BlockSuite可以实现两个进一步的目标:
使用任何UI框架组装复杂的协作应用程序。
将整个协作编辑器轻松嵌入其他应用程序中。
这通过两种方式来演示:
BlockSuite中的块树提供了基于订阅的API,允许将其用作具有不同UI框架的状态存储。这提供了类似于使用流行的状态管理库的开发体验。
BlockSuite附带的AFFiNE编辑器使用Web组件构建其基础块组件。这意味着不仅可以使用任何UI框架来开发AFFiNE中的块,而且编辑器还可以作为标准化Web组件嵌入到任何框架中。
但是,在BlockSuite中的渲染能力不仅限于DOM。例如,为了支持AFFiNE中的白板功能,BlockSuite实现了Phasor渲染库。该库使得CRDT模型可以呈现到画布上,并允许其与主文档共享历史状态。 总之,BlockSuite在渲染方面提供了出色的灵活性。它允许与各种UI框架无缝集成,并提供将协作编辑器轻松嵌入其他应用程序的方式。
本地优先范式
BlockSuite框架作为AFFiNE知识库的基础,类似于Chromium引擎驱动Chrome浏览器。然而,BlockSuite不仅是独立的开源的,还结合了实用的基础架构设计,以确保其多功能性。这个决定源于我们的信念,AFFiNE为新应用程序架构设定了基准,而BlockSuite可以作为基于这个架构构建产品的通用框架。这个架构被称为本地优先
什么是本地优先?
在网络应用程序出现之前,最经典的应用程序形式是本地应用程序。它的功能不会受到网络延迟的影响,可以快速将内容保存到本地文件中,使用户对数据拥有完全的控制权。但是,在协作方面,不同用户对各自的本地文件进行更改时很容易发生冲突。
后来,浏览器的广泛采用使得网络应用程序普及开来。这些应用程序在很大程度上依赖于集中式服务器基础设施以促进多个用户之间的协作。然而,一个明显的缺点是它们依赖于互联网连接,其数据受到封闭平台的控制。
相比之下,本地优先应用程序提供了本地低延迟操作和全面的协作能力。这允许(请注意,这不是强制性的)完全在本地设备上存储应用程序数据,而不需要网络连接进行功能。然而,当连接到互联网时,它们会无缝地继续协作,提供了将本地和网络应用程序的优点结合起来的平滑体验。
image
要实现本地优先应用程序的理想体验,需要进行技术突破,这个突破是CRDT(无冲突复制数据类型)。
本地优先和CRDT
为什么CRDT对于本地优先应用程序至关重要?因为它的数据模型解决了本地应用程序协作时出现的冲突问题。CRDT提供类似于JavaScript中的标准ArrayMap的数据结构,可用于模拟应用程序状态,如下所示:

typescript

// Yjs是BlockSuite中使用的CRDT库
import * as Y from 'yjs';

const yMap = new Y.Map();
const yArray = new Y.Array();

yArray.push('hello');
yMap.set('world', yArray);
乍一看,这似乎与使用标准JavaScript数据结构没有区别。就像JavaScript中的数组和对象可以序列化为JSON一样,CRDT状态也可以序列化为类似于protobuf的二进制协议。但关键点在于,任何操作如yArray.push都可以在此格式中逐步编码并分发,类似于git补丁。
image
最重要的是,CRDT的算法保证了来自多个用户的并发更新,无论合并的顺序如何,始终会产生一种最终一致的状态,类似于git但永远不会冲突。例如,如果三个用户A、B和C分别在本地生成自己的补丁,则无论所有补丁合并的顺序如何,所有用户都会获得最终一致的状态。这种状态合并独立于每个用户的设备。因此,我们可以消除服务器成为真理的权威来源的必要性。
image
本地优先在AFFiNE中
为了帮助构建像AFFiNE这样的本地优先应用程序,BlockSuite已经充分利用CRDT来构建其块树。基于这种方法,AFFiNE中存储在本地的内容不再直接使用人类可读的文件格式,而是使用从CRDT块树序列化的二进制格式。这使AFFiNE能够支持一些独特的功能:
网络成为完全可选的,就像可以在没有GitHub的情况下使用git一样。
只要两个客户端建立对等连接,就可以在没有服务器的情况下进行协作。
以CRDT格式导出的知识库文件始终保持协作状态,只需要合并即将到来的补丁,即可继续协作。
这些特征对于像AFFiNE这样的知识管理应用程序尤为重要。但是,即使您正在开发的应用程序不是特定的知识库,本地优先的原则仍然有意义吗?我们相信答案仍然是肯定的。
云基础应用程序中的本地优先
在基于服务器的应用程序领域,开发人员仍然可以利用CRDT,本地优先应用程序的基本方面,轻松集成强大的实时协作功能。
除此之外,CRDT还为应用程序开发人员提供以下优点作为状态管理工具:
开箱即用的历史记录跟踪:CRDT允许简单地在历史状态之间遍历,类似于git checkout,无需复杂的状态管理基元。
增量UI更新:CRDT的数据模型已经提供了类似于虚拟DOM的差异操作能力,使基于状态更新的清晰的单向数据流成为可能。
可插拔的数据同步:CRDT二进制文件在不同的网络协议之间的同步可以由提供者处理,消除了在应用程序逻辑中显式网络操作(例如发送HTTP请求)的需要。只需连接到提供者,同步就会得到处理!
通过完全使用CRDT实现其数据层,BlockSuite不仅提供了实时协作功能,而且还具有上述优点,促进了协作应用程序的开发。
如果您有兴趣了解如何在基于服务器的应用程序中使用BlockSuite,请参阅数据持久化一节获取更多信息。
这是未缩进内容