一、从一个具体的问题开始

假设你要在鸿蒙上做一个笔记应用。产品经理给你画了这样一页原型:

  • 标题、段落、待办清单(能勾选);
  • 列表能缩进成多级;
  • 图片能半宽两张并排;
  • 选中文字能加粗、加链接;
  • 任何操作能撤销;
  • 杀后台不丢内容;
  • 下一期要接 AI:选中一段话,让 AI 改写。

这个需求清单在 2026 年的移动开发里再普通不过。但在 HarmonyOS NEXT 上,你会发现没有任何一个现成组件能承接它。系统给你的 RichEditor 是字符级编辑控件——它能处理"一段带样式的文本",但它不知道什么是"块"、什么是"文档树"、什么是"第 3 级列表项"。你需要的不是一个更好的文本控件,而是一整个缺失的层:文档模型层

这就是 ArkBlocks 项目存在的原因。它不是一个笔记应用,而是补上这一层的框架:文档模型、事务引擎、撤销重做、序列化、插件系统,全部用 ArkTS 从零实现,渲染走原生 ArkUI。笔记、知识库、AI 写作、Markdown 编辑器、文档工具,都构建在它之上。

在展开架构之前,先老老实实盘点:为什么现有三条路都走不通。

二、三条现成的路,各自死在哪里

路一:WebView + Web 编辑器(Tiptap / Quill 塞进 WebView)。

这是最快出原型的方式,也是技术上最贵的方式。问题不是"能用不能用",而是你从此要把每一个编辑器核心体验都架在一层桥接之上:

  • 输入法:中文 composing(拼音串)、下划线预编辑、候选词期间的光标语义,全部发生在 Web 层,宿主应用拿到的事件是二手的。编辑器最核心的 IME 交互被隔离在你控制不了的一层。
  • 双端状态同步:Web 层有一份文档,ArkTS 层想持久化、想接 AI、想做协同,就得不断跨越 JS Bridge 搬运数据。每一次搬运都是一次序列化 + 一次跨运行时调用。
  • 手势与平台一致性:长按选择、拖拽、滚动回弹、字体缩放,WebView 的行为和原生应用总有微妙差异——用户未必说得出哪里不对,但感受得到。
  • 无障碍:屏幕阅读器读的是 ArkUI 组件树,WebView 内部的内容对无障碍体系是半透明。

路二:直接用系统 RichEditor

它确实是原生的,也确实能打字、加粗、撤键。但把需求清单放上去逐条对,会发现它提供的是"编辑控件"而非"文档模型":

需求RichEditor 给了什么缺什么
标题/段落/列表不同样式的 span没有"块类型"概念,样式即内容,语义无从谈起
多级嵌套列表缩进是排版概念,不是树结构
块级操作(拆分/合并/移动/嵌套)Enter 拆块、Backspace 合块、拖拽排序全要自己造
撤销/重做控件内部黑盒与你自己的数据层对不上,无法和数据变更共享同一套历史
持久化拿不到结构化文档,只能导出带样式文本
AI 接入AI 想要的结构化上下文("第 2 节的第 3 个待办")不存在

一句话总结:RichEditor 解决的是"字符怎么显示、怎么输入",而你的应用卡住的地方是"文档是什么、怎么变、怎么存"。这中间差的那一层,就是 ArkBlocks 要写的。

路三:每个团队从零自研。

缺什么补什么,看似自由,实则是在没有地图的情况下重走一遍别人走过的三十年:文档模型怎么定、事务边界画在哪、撤销存快照还是存逆操作、剪贴板放几种格式、Markdown 转换怎么处理有损……每一个都是独立的坑位,而且自研件永远排不上重构的优先级——它"能跑",直到有一天不能。

三条路的结论:需要的是一个原生、可复用、以块为中心的编辑器框架。接下来看这个东西长什么样。

三、Block 范式:把文档从"字符串"升级为"树"

传统富文本把文档当作"一长串带样式的字符";Block 编辑器把文档当作一棵由类型化块组成的树。ArkBlocks 里,一个文档在内存中就是这样:

[
  {
    "id": "b1", "type": "heading",
    "props": { "level": 1 },
    "content": [{ "type": "text", "text": "会议纪要", "styles": {} }],
    "children": []
  },
  {
    "id": "b2", "type": "bulletListItem",
    "props": {},
    "content": [{ "type": "text", "text": "确认排期", "styles": {} }],
    "children": [
      {
        "id": "b3", "type": "checkListItem",
        "props": { "checked": true },
        "content": [{ "type": "text", "text": "联系供应商", "styles": {} }],
        "children": []
      }
    ]
  }
]

注意这个模型里每一块都有:稳定 ID(UUID v4,创建即冻结)、类型名(受 Schema 校验)、类型化属性行内内容(文本 + 样式 + 链接的扁平序列)、子块(无限嵌套)。这个看似简单的结构,一次付清了后面所有的账:

  • 块级交互免费获得:拆分、合并、嵌套、拖拽、折叠,全是树操作;
  • 语义免费获得:"把所有 checked=true 的待办筛出来"是一行查询,对 span 模型这是不可能任务;
  • 协同与评论有锚点:外部引用(评论、书签、AI 标注)挂在块 ID 上,块移动、撤销后 ID 不变,引用就不断;
  • AI 有操作单元:AI 的上下文是块树、AI 的输出是块树、AI 的流式写入是往树里提交事务——这是"选中改写""继续写作"这类功能的地基;
  • 序列化无损:JSON 就是文档本身,不是导出格式。

四、ArkBlocks 是什么、不是什么

不是:笔记应用、UI 组件库、WebView 壳。 :一个分层引擎 + 一个唯一入口。整张架构图:

                        ┌──────────┐
                        │  Editor  │  ← 唯一外观层 / 公共入口
                        └────┬─────┘
       ┌──────────┬─────────┼──────────┬───────────┐
       ▼          ▼          ▼          ▼           ▼
  Document    Schema    Transaction   Selection    History
  (Block 树)  (规格)   (唯一变更边界)
       │                                                 Plugin Manager
       ▼                                                      │
  Command / Renderer ◄── diff ── Transfer ◄── Adapter ◄── Clipboard
  Dispatcher  Registry                        (平台层)

两条规则撑起整个架构:

规则一:Document 是唯一事实来源。 选区、历史、渲染器、剪贴板持有的都只是派生数据——选区只存 (blockId, offset) 坐标,历史只存操作逆序,剪贴板只存复制时刻的快照。文档内容在内存里只存在一个地方,"打了一个字要同步几处状态"这个经典难题被从结构上消灭。

规则二:变更只走一条路。 任何修改——包括撤销、包括插件、包括 AI 写入——都必须经过事务:

输入 → Adapter → Command → Transaction → Document → History → Selection → Renderer

配套的是一张依赖矩阵:核心引擎层(Document / Transaction / History)禁止 import 任何平台 API 与上层模块;所有 @ohos.* 调用只允许出现在 Adapter 层。收益立刻兑现:核心引擎在无真机环境下可完整单测,平台能力替换(比如未来折叠屏形态的输入适配)不动核心。

Editor 是全框架唯一允许跨层 import 的类,聚合 12 个子系统,也是应用唯一需要认识的类型。

五、和 Web 编辑器掰手腕:四个架构级的不同

ArkBlocks 研究过 ProseMirror、Lexical、TipTap、BlockNote,但每个决策独立重做。挑四个最有分量的差异:

1. 单层 Block 树 vs 双层表示。 ProseMirror 内部是一棵低层 node 树,应用层 API 是它的投影,两层之间靠双向转换(BlockNote 里真实存在 blockToNode / nodeToBlock 这样的转换函数)维持一致。转换层是这类架构永久的 bug 温床——两个表示稍有漂移,用户看到的内容和撤销恢复的内容就会不一致。ArkBlocks 没有历史包袱,Block 树本身就是文档模型,零转换。

2. 自建事务系统 vs 复用 PM Transform。 ArkBlocks 的操作集是有限枚举——20 种类型化操作(insert / remove / move / split / merge / indent / outdent / insertInline / deleteInline……),用 ArkTS 联合类型表达,编译器就能检查完备性。换来两个好处:核心零第三方依赖(纯 ArkTS 运行时,无 polyfill 无桥接);逆操作生成规则清晰,撤销重做不依赖任何外部库的内部行为。

3. Adapter 隔离 vs 平台 API 散落。 Web 编辑器的"平台"是浏览器,天然后端统一。鸿蒙的形势复杂得多:RichEditor、pasteboard、振动器、文件沙箱、窗口避让,全是独立子系统。ArkBlocks 把它们全部关进 7 个子适配器(Input / Clipboard / Keyboard / Gesture / Focus / Window / Accessibility),每个都实现核心声明的接口——测试用 Mock 替换,换设备形态换适配器。

4. 渲染走注册表,不走动态加载。 ArkUI 静态编译,不能像 Web 那样运行时动态加载组件。ArkBlocks 用 blockType → 渲染分类 的注册表分发(目前四类:editable / divider / card / image),插件注册类型,渲染层按分类路由到具体组件。官方插件和第三方插件走同一条注册通道——如果官方插件需要开后门才能实现,按项目规定视为 SDK 不完整,补 SDK 而不是开后门。

维度ProseMirror / BlockNoteArkBlocks
运行平台浏览器 DOMHarmonyOS NEXT 原生
文档模型低层 node 树 + Block 投影(双层)Block 树(单层,零转换)
事务系统PM Transform自建,20 种类型化操作
平台依赖DOM/浏览器事件全部隔离于 Adapter 层
插件隔离直接访问编辑器状态PluginContext 受控边界
渲染DOM 动态创建静态编译 + 注册表分发

六、30 秒看 API:用起来是什么样

import { Editor, EditorConfig } from '../editor';
import { DefaultBlocksPlugin, DefaultStylesPlugin, HistoryPlugin } from '../plugin';

// 1. 创建:注册块类型与插件,引擎自动装配 12 个子系统
const editor = Editor.create({
  plugins: [DefaultBlocksPlugin, DefaultStylesPlugin, HistoryPlugin],
  initialContent: [{ type: 'paragraph', props: {}, content: [], children: [] }],
} as EditorConfig);
editor.mount();

// 2. 高层变更:命令
editor.exec({
  type: 'insertBlock',
  blockType: 'paragraph',
  position: { type: 'after', referenceId: 'block-1' },
});

// 3. 底层变更:事务(原子提交,一步撤销)
editor.transact((tx) => {
  tx.addOperation({ type: 'updateProps', blockId: 'h1', props: { level: 2 } });
  tx.addOperation({ type: 'insertBlock', block: newBlock,
                    position: { type: 'end', parentId: '__root__' } });
});

// 4. 撤销 / 重做
editor.undo();
editor.redo();

// 5. 持久化:JSON 即文档
const json = JSON.stringify(editor.toJSON());

注意第 3 步:一个事务里打包的多个操作要么全部生效、要么全部不生效,撤销时一次回退。这是后面所有高级能力(AI 一步撤销整段生成内容、拖拽跨层移动)的同一块地基。

再用一次回车键的完整生命周期,把整条流水线串起来——这是理解本系列后续所有文章的钥匙:

用户按下 Enter(RichEditor)
  → InputAdapter 捕获,翻译为规范意图
  → CommandDispatcher 生成 splitBlock 命令
  → TransactionManager 开启事务
      ├─ Phase 1 校验:结构 + Schema + 操作级检查(任一失败,整个事务作废)
      ├─ Phase 2 执行:前块保留原 ID,新块用预分配的 UUID 拿走后半段内容
      └─ 应用操作的同时生成 RendererDiff(哪些块被插入/更新/移动)
  → 逆操作压入 History(这一步撤销的边界就此划定)
  → Selection 清理失效引用
  → 渲染层只重建 diff 中列出的块,其余组件原样复用

一次按键,九步,每步可测、可拦、可撤销。编辑器工程的全部难度,就在于让每一次这样的按键都严格走完这条路,没有捷径。

七、诚实的现状盘点:现在能做什么、不能做什么

写架构文最忌讳画大饼。以下是仓库当前的真实边界。

已落地(Playground 可真机演示):

  • 10 种内置块:段落、标题(H1–H3,可作折叠容器)、无序/有序/待办列表、引用、Callout、分隔线、卡片、图片;
  • 嵌套与折叠容器、层级引擎(嵌套/取消嵌套)、拖拽排序;
  • 有序列表动态序号解析、按层级渲染的列表标记;
  • 卡片与图片的半宽并排布局;
  • 斜杠菜单、Block 操作面板(左滑)、行内选择工具栏(加粗/删除线/高亮/链接)、移动端插入面板;
  • 传输引擎(剪贴板多格式、块树完整复制粘贴)、撤销/重做;
  • 文档运行时:本地文件持久化、空闲+退后台自动保存、冷启动恢复;
  • 反馈引擎:语义化操作的触感与音效(含失败降级);
  • 插件 SDK MVP,Image / Card / Callout 已迁移为官方插件。

未落地(里程碑表中明确未开始,本系列会如实标注):

  • 选区引擎(M3):跨块选择、SelectionResolver 尚为骨架;
  • 正式渲染器(M5):渲染分发目前由 Playground 演示层承担;
  • RichEditor 正式同步协议(M6):当前基于 Playground 集成层经验,同步协议被列为最高优先级开放风险;
  • AI 集成(M7)、表格等高级块(M8 后续)。

项目处于 1.0 之前的 API 稳定化阶段,哪些 API 已冻结见仓库 docs/spec/API-STABILITY-v1.md

八、规格驱动开发:一个框架如何防止腐烂

最后讲工程方法,因为这决定这个项目第五年还在不在。

ArkBlocks 执行严格的规格驱动开发(SDI),任何功能先过这条流水线:

研究 → 架构 → RFC → 规格 → 规格测试 → 实现(PR) → 独立评审 → 评审修复 → 合并

几个关键机制:

  • 决策优先级链:冲突时 RFC > 规格 > 规格测试 > 现有实现。实现永远不是事实来源;实现中发现规格有矛盾,正确动作是停下实现、开新 RFC,而不是"顺手改一下"。
  • 冻结契约docs/research|design|rfc|spec|tests 全部冻结,常规开发不许动。架构变更只走正门(新 RFC)。
  • 无回归测试,不合并:评审发现的每个 Bug 必须至少引入一个回归测试。
  • 每个 PR 只做一个架构目标:20 余个实现 PR 几乎是一条直线,没有返工大潮。
  • 平台知识单独沉淀:真机才会暴露的知识(el2 沙箱的文件系统怪癖、RichEditor 同步时序、振动器错误码语义)记录在 MEMORY.md,含每次踩坑的根因、失败尝试与最终方案——下一位工程师(无论人或 AI)动相关代码前必读。

到本文写作时的工程数据:6 份 ADR、3 份 RFC、1 份核心规格、280 个规格测试、20 余个实现 PR、约 689 个真机手工测试用例、0 个未关闭的 Critical 缺陷

多角色协作上,项目让不同的 AI 会话分工扮演 Architect / Framework Engineer / Reviewer / Maintainer——写代码的会话不审自己的代码,评审发现的每个问题回到规格层面归因。AI 在这个流程里不是替代工程纪律,而是被纪律约束的角色。

九、本系列路线图

接下来 12 篇,按依赖顺序拆解这个框架,每篇独立成文:

  • 引擎核心(02–04):Block 树与双 O(1) 索引的文档模型;20 种操作 + 两阶段提交的事务引擎;存逆操作不存快照的撤销栈。
  • 扩展体系(05–07):Schema 注册与运行时校验;插件 SDK 的五阶段生命周期与受控边界;属性引擎——插件声明元数据,UI 自动生成。
  • 渲染与平台(08–12):ForEach 局部刷新的真机教训;RichEditor 文本同步四规则;剪贴板多格式策略;自动保存与 el2 沙箱踩坑;触感反馈的探测-降级模式。
  • 方法论(13):规格驱动 + 多角色 AI 协作的完整复盘。

如果你正在鸿蒙上做任何涉及"结构化内容"的产品——笔记、文档、工单、IM 富卡片、AI 写作——这个系列想让你下次立项时,手里多一张"原生框架"的牌。

下一篇:《文档模型:Block 树是唯一事实来源》——拆解 RFC-0001 的三层数据结构、UUID 身份规则,以及"树 + 双索引"如何在 O(1) 和 O(树深) 之间取得平衡。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐