-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- 体系定位:MH 将编辑器视作“可动画的持久化数据结构”,以不可变节点树 + 事务操作串行化,奠定了现代复杂交互的框架无关内核思路。
-
代表工程:
CodeMirror、ProseMirror、Lezer解析器、Eloquent JavaScript书籍,构成从语言教学到运行时内核的完整链条。 - 设计哲学:强调“功能核 + 可拔插外围”,以严格 schema、插件协议、事务日志组成的分层体系,在富文本、协作、图形、低代码等领域被广泛复制。
- 数据即语法树:全部状态都抽象为 typed node tree;变更是对树的最小 Step,易于验证和 replay。
-
事务化操作流:单次用户交互被封装为
Transaction,包含前置条件、变更、meta 信息,天然支持撤销、协作与 time-travel。 - 插件协议:核心只暴露 hook(state lifecycle、view lifecycle、DOM 事件映射),任何能力(输入法、keymap、协作游标)皆以插件装配,确保演进不破坏内核。
- 不可变 / 增量渲染:状态不可变,但视图分片 diff(decorations、viewport slicing),兼顾正确性与性能——为 React Concurrent Mode 提前提供思路。
- Schema 驱动验证:内容模型用 DSL 严格定义,可在编辑时阻断非法结构;这一做法后来引导 TipTap、低代码平台的强类型校验。
- 操作先于状态:强调 OT/CRDT “转换操作”而不是同步状态;协作冲突靠 Step/Transform 组合解决,启发 Automerge、Yjs。
- 图形/白板系统:事件总线 + HistoryManager,模型与 UI 彻底分离,Excalidraw/React-Diagrams 直接复用该思路。
- DSL/低代码:事务化 DSL 校验(禁止删除被引用组件)、强 schema 审批,来源于 ProseMirror 的 node constraint。
- 游戏与交互式应用:命令模式撤销重做、集中输入映射、可组合事件处理器,Phaser 等引擎在编辑器工具链中采用。
-
教育与知识传递:
Eloquent JavaScript把函数式和状态机理念灌输给初学者,折射其“严谨模型 + 可执行范式”哲学。
- 先写概念文档:参考 ProseMirror Principles,先定义对象、约束、操作,再写代码,降低复杂系统维护成本。
-
分层内核:把功能拆成
State(不可变数据)、Transaction(操作语义)、View(渲染),保证可测试与替换。 - 插件即能力:用显式 hook 管理扩展点,让团队能并行开发而不互相冲突。
- 精细增量更新:只重绘受影响的片段(range/decorations),在任何重度交互场景都能保持性能稳定。
- 操作可组合:设计 Step/Command 应满足可序列化、可撤销、可合并,便于实现协作、离线、审计等增值特性。
- Schema 与运行时绑定:让 schema 不只是验证,而是驱动输入法、菜单、快捷键等交互逻辑,实现“强一致 UX”。
很多人只把 Marijn Haverbeke(MH)当作几款著名编辑器的作者:CodeMirror、ProseMirror、Lezer、Acorn、Tern、《Eloquent JavaScript》……
但如果从前端架构与交互系统设计的视角重新看,你会发现他更像是一个**“复杂状态系统设计哲学”**的奠基人。
你引用的资料已经讲了他在不可变状态树 / 事务化操作 / 插件架构上的开创意义。下面我从几个额外的、但同样重要的维度,把他更完整的架构理论与设计哲学补全成一篇介绍。
MH 对编辑器的看法可以概括为一句话:编辑器不是一堆 DOM hack,而是一个显式的、可推理的状态机。
-
数据优先,而不是 UI 优先
- 传统 WYSIWYG 编辑器:直接操作 DOM /
contentEditable,UI 即 State,逻辑高度耦合,难以推理。 - MH 的做法:
- 定义一个完整的文档状态模型(树、节点、标记等),作为唯一真实来源(Single Source of Truth)。
- 所有用户操作抽象成“对文档状态的变换(Steps/Transactions)”。
- 视图仅仅是文档状态的投影,而非数据本身。
- 传统 WYSIWYG 编辑器:直接操作 DOM /
-
状态机而不是事件堆
- 每次用户输入 / 粘贴 / 删除 / 协作同步,本质都是:
[ State_{n+1} = f(State_n, Operation) ] - 这套思想在
ProseMirror和CodeMirror 6里被贯彻到极致:- 编辑器核心不关心 DOM,只关心状态流转。
- DOM 更新是一个“副作用层”,被严格限制在 View 层。
- 每次用户输入 / 粘贴 / 删除 / 协作同步,本质都是:
对前端工程师的启发:
复杂前端应用(富文本、画布、低代码、流程引擎)最好先画清楚状态机和操作模型,再考虑 UI,不要从组件堆砌开始。
在 ProseMirror 中,他做了一个非常重要但经常被忽视的设计:用 Schema 强力约束文档结构。
-
Schema 驱动的文档结构
- 每个节点类型可声明:
- 能包含哪些子节点
- 哪些 Mark(加粗、链接、下划线等)合法
- 哪些节点可以互相嵌套
- 所有编辑操作,都必须保持文档结构合法,否则视为非法 Step。
- 每个节点类型可声明:
-
不可变、持久化数据结构
- 文档树是不可变的,每次操作不会原地修改,而是返回一个新版本(结构共享)。
- 好处:
- 天然支撑 Undo/Redo(历史版本链就是状态链)。
- 易于做时间旅行、调试回放。
- 协作冲突处理更容易,因为每一步都是清晰的“旧状态 → 新状态”的映射。
-
“非法状态不可表示(Illegal States Are Unrepresentable)”
- 这是他在设计中非常偏好的理念:
通过类型与 Schema 设计,让错误状态在逻辑层就“构造不出来”。 - 对标很多系统是“允许一堆乱七八糟的状态,然后在 UI / 校验里兜底补救”。
- 这是他在设计中非常偏好的理念:
对工程与面试的价值:
当你设计表单引擎 / 流程引擎 / 图形编辑器时,如果能说出“我们会定义一个 Schema 约束所有合法状态,让非法状态根本构造不出来”,这是一种高成熟度架构思维,很多高级职位面试官会非常买账。
你在资料中已经提到了 Step / Transaction / Plugin,这部分其实可以更系统地理解为:
-
Step:最小原子操作
- 例如:
- 插入一段文本
- 删除一段范围
- 包裹为某种节点或 mark
- 每一个 Step:
- 是可序列化的(便于网络传输)
- 有明确的
apply方法:newState = step.apply(oldState) - 通常还伴随一个 inverseStep,用于撤销
- 例如:
-
Transaction:有语义的操作组合
- 用户一次“看得见的”操作(如输入一段中文、粘贴一段富文本)通常对应多个 Step。
- Transaction 负责:
- 打包多个 Step
- 附带 metadata(例如:是否加入 history、来源是本地还是远端、是否需要滚动到可视区域等)
- Transaction 带来的最大好处:
可以给操作赋予更高层语义,而不只是机械的 DOM 变动。
-
History:时光机,而不是快照列表
- History 维护的是一串 Transaction 的逆操作,而不是整个文档的完全快照。
- 这让:
- Undo/Redo 成本与操作数相关,而不是与文档大小相关。
- 为协作系统提供了“可变换的操作流”,方便与 OT/CRDT 集成。
这套思想的延展:
- 任何需要撤销重做的系统(画布、流程图、状态机编辑器、低代码布局器)都可以复用:
- 操作日志 + 可逆操作 + 事务化组合这个模式。
- 相比很多简单系统用“深拷贝整个 state 放进栈里”,MH 的做法是从操作层做抽象,更稳健、更高维。
在 CodeMirror 6 与 Lezer(增量 parser)中,MH 非常强调一个核心理念:不要为不影响用户体验的计算浪费时间。
-
增量解析(Incremental Parsing)
-
Lezer的 parser 会维护一个树结构,并在输入变动后,仅重新解析受影响的局部,而不是整文件。 - 好处:
- 大文件依然能在每次击键后保持流畅。
- 语法高亮、错误提示等可以实时更新,而不会卡顿。
-
-
视口(Viewport)驱动渲染
- 文本编辑器永远只渲染“屏幕上看得到的那几十行”,其余部分只保留数据,不生成 DOM。
- 这和虚拟列表(virtualized list)的思路类似,但在编辑器场景下更复杂(需要精确定位光标、选区、软折行等)。
-
精细化 DOM 更新
- 编辑操作会被映射到最小化的 DOM 差异,避免大范围重绘。
- 在
CodeMirror 6中,视图层使用了一种接近“局部虚拟 DOM”的机制,但比 React 更低层、更针对编辑器场景。
对前端工程实践的启示:
- 真正复杂且性能敏感的系统,往往需要:
- 显式建模“增量计算”路径
- 显式建模“视口/可见区域”
- 你可以把这当成“把 React 的 reconciliation 思想推广到你的业务状态和数据结构上”。
MH 一直坚持的一个架构风格是:把核心做到极小、稳定,然后把绝大部分特性做成插件。
-
“编辑器本身什么都不做”
- 原生
ProseMirror/CodeMirror 6核心几乎不提供“业务功能”:- 不内置快捷键
- 不内置特定语言支持
- 不内置特定工具栏
- 一切都是通过 Plugin / Extension 注入:
- Keymap 插件
- 历史管理插件
- 协作插件
- 自定义 NodeView / Widget 插件
- 语言服务插件(在
CodeMirror 6中)
- 原生
-
插件是“一等公民”,而非“附加钩子”
- 核心数据结构(State)直接为插件预留了“State Field”、“View Plugin”等扩展点。
- 插件可以:
- 持有自己的私有状态
- 订阅 Transaction
- 参与文档渲染(通过 Decoration / NodeView)
-
组合优先,而非继承
- 没有庞大的继承层次,而是通过一组 Extension/Plugin 列表来组合出某种编辑体验。
- 这和 React Hook / 中间件体系精神类似:功能粒度更细,通过组合构造复杂行为。
对项目架构的启示:
- 任何大前端系统(例如:低代码平台、流程引擎、画布编辑器)都可以借鉴:
- 把核心做成一个最小状态机
- 业务与扩展通过插件/中间件机制挂载上去
- 形成“可裁剪 / 可增量演化”的架构
面试中,若被问到“你会怎么设计一个可插拔的富文本/画布系统”,围绕小内核 + 插件化扩展点 + 组合式配置来回答,是非常高级的答案。
除了编辑器,MH 还写了大量语言工具:Acorn(JS parser)、Tern(静态分析 / IDE 补全)、Lezer(通用 parser)。
背后的共同哲学是:工具的基础是一个可靠、可组合的语法与语义模型。
-
最小但完整的抽象
-
Acorn是一个非常“小”的 parser,但:- 完整覆盖 ECMAScript 语法
- 对标准变化快速响应
- 保持模块化,使其易于嵌入各种工具链
-
Tern也是最小化设计:用尽量少的配置,构建一个能供编辑器使用的静态分析模型。
-
-
可恢复错误的解析(Error Recovery)
- 在编辑环境中,代码往往是“不完整的”:
- 缺括号
- 写到一半的表达式
- MH 的 parser 设计会在错误处智能恢复,继续构建 AST:
- 这对 IDE 来说极其关键,否则任何半写的代码都会让工具瘫痪。
- 在编辑环境中,代码往往是“不完整的”:
对前端工程师的启示:
- 当你设计 DSL(表单配置、工作流语言、低代码描述语言)时:
- 不要只考虑“合法代码长什么样”
- 还要考虑“非法 / 不完整的输入如何被优雅处理”
- 可恢复的 parser、清晰的 AST 设计,是很多高级系统的必备能力。
MH 的作品有一个共同特点:代码和文档都非常“讲道理”。
-
论文级文档 + 可读源码
-
ProseMirror的 principles 文档,基本是一篇“论文 + 工程设计说明书”。 - 源码中大量注释,解释“为什么要这么设计”,而不仅仅是“这么用”。
-
-
偏爱函数式与纯度
- 在《Eloquent JavaScript》以及他的库设计中,你会看到:
- 较多不可变数据结构
- 纯函数式更新
- 清晰分离“计算逻辑”和“副作用”
- 这让系统具备:
- 更易测试(输入 → 输出,可预测)
- 更易调试(Transaction 可回放)
- 更易重构(依赖关系清晰)
- 在《Eloquent JavaScript》以及他的库设计中,你会看到:
-
通过真实使用来“证明”设计
-
CodeMirror、ProseMirror等项目,都是在长期被真实项目使用中不断打磨:- 设计不是一次性定死,而是在实际复杂场景中不断矫正。
- 他非常强调“简单到可以解释、严谨到经得起大量使用”。
-
如果你想在工程能力 / 架构思维 / 面试谈吐上向 MH 靠近,可以刻意练习几个方向:
-
(1)刻意训练“状态机视角”
- 看到一个需求(富文本、画布、审批流程、拖拽排序)时,先问:
- 我能不能把它抽象成一个状态机 + 操作集合?
- 状态有哪些?操作有哪些?哪些操作是可逆的?
- 看到一个需求(富文本、画布、审批流程、拖拽排序)时,先问:
-
(2)为你的系统设计“Schema 与不变量”
- 学 ProseMirror:
- 给你的业务数据(表单 / 工作流 / 图形模型)设计一个 Schema。
- 明确列出:什么状态是合法的?有哪些状态是禁止出现的?
- 把“非法状态不可表示”变成架构目标,而不是事后补救。
- 学 ProseMirror:
-
(3)为复杂操作引入 Transaction 概念
- 不要只在 reducer / store 里做一堆 flag 切换。
- 而是定义:
- 原子操作(Step)
- 大粒度操作(Transaction)
- 并在日志中记录 Transaction,用于:
- Undo/Redo
- 调试回放
- 协作同步
-
(4)学习插件式设计
- 设计任何可扩展系统时问自己:
- “这部分能不能做成插件 / middleware / extension?”
- “核心能否保持极简?只提供扩展点,而不提供具体业务逻辑?”
- 设计任何可扩展系统时问自己:
-
(5)向下钻研一两个 MH 的实际项目
- 推荐顺序:
- 看
CodeMirror 6的 State / View / Extension 设计 - 看
ProseMirror的 Transaction / Step / Schema 设计 - 边读边在脑子里对照:如果让我来实现,我会怎么做?我会踩哪些坑?
- 看
- 推荐顺序:
综合来看,MH 的经典架构理论与设计哲学可以浓缩成几条对前端非常关键的“隐形标准”:
- 数据优先、UI 是投影:编辑器 / 画布 / 低代码都应该有独立的状态机与操作模型。
- Schema 与不变量驱动设计:让非法状态“构造不出来”,而不是在 UI 层打补丁。
- 操作中心、事务化、可逆:用 Step / Transaction / History 重构对“修改”的理解。
- 增量与局部性:只为真正变动的部分付费,结构上支持增量计算与局部渲染。
- 插件化内核、组合式扩展:小内核 + 高度可组合的插件体系,是复杂系统的最佳形态之一。
- 简单但可验证:每个设计都有清晰的解释路径和可验证性,而不是“只要能跑就行”。