18 KiB
ETAF 模块职责与目标架构(设计提案,未实现)
状态:设计提案。本文不是当前公共 API 或已交付实现的规范;当前可用契约仍以
architecture.zh.md、user-guide.zh.md和实际测试为准。
本文把 ETAF、Ebox、ECSS、TP、etaf-ui、etaf-sqlite、性能工具和两个
Playground 的职责收敛成一份待评审的边界提案。它描述的是从零设计时应保持的
模型,不是把当前实现中的每个文件名当成架构真相。
文中标有“目标”的部分是后续演进方向;除非代码、测试和基准同时完成,不能 把目标 API 描述成已经交付的功能。提案没有自动授权实现,也不改变当前 Host 注册表、View 语法或包依赖。
1. 最小稳定模型
ETAF 需要区分四种表示,而不是把它们都叫作“组件”或“box”:
Component 语义与所有权
↓
Text / Box View 结构
↓
Ebox 几何与渲染计划
↓
TP / Emacs 最终提交
1.1 公共概念
用户只需要学习三个主要概念:
| 概念 | 稳定职责 | 不负责的事情 |
|---|---|---|
Text |
文本内容、字体、inline text properties、换行入口 | 子树布局、业务状态、生命周期 |
Box |
尺寸、背景、边框、padding、子节点和布局 | Component identity、Data、Action |
Component |
可复用语义、props、slot、状态、Context、Action、生命周期 | 像素测量和布局算法 |
Host 是 Renderer 内部用于称呼可直接降低到后端的原语,不是用户需要额外
学习的第三套视觉分类。Ebox Node 则是更底层的几何/渲染对象;它可以由一个
Text、一个 Box,或一棵 Component 子树产生。
1.2 Text 与 Box 的关系
CSS 中所有最终可见元素都会产生 box,但 CSS 生成的 box、ETAF 的 View 原语和
Ebox 的后端节点不是同一棵树。inline 和 block 描述盒子参与父布局的方式;
flex 和 grid 描述盒子内部如何排列子节点。
因此 ETAF 不应该把这些概念做成 Component,而应在 Box 上保留两个正交属性:
:outer = inline | block
:layout = normal | row | column | flex | grid
:outer 表示这个 Box 如何参与父级的普通布局;:layout 表示这个 Box 如何
排列自己的子节点。normal 是 ETAF 用户层对普通内容布局的名称,Renderer
可以把它降低为 Ebox 内部的普通内容路径。
目标公共模型是:
View = Text(content, typography)
| Box(layout, surface, children)
| ComponentCall(props, slots)
目标中 ETAF 对用户暴露的 Box 内部布局模式是封闭集合:
normal | row | column | flex | grid
普通 Box 默认是 :outer 'block :layout 'normal,不会隐式变成 column。
需要纵向堆叠时明确写 :layout 'column;需要横向、Flex 或 Grid 时明确选择
对应模式。Text 默认作为 inline content 参与 normal 布局,但自身负责文字
测量、换行和 inline runs。
语义映射大致为:
| CSS 语义 | ETAF 目标表达 |
|---|---|
| inline text/run | text |
| 普通 block 内容 | (box :outer 'block :layout 'normal ...) |
| 普通 inline 内容 | (box :outer 'inline :layout 'normal ...) |
| 纵向排列 | (box :layout 'column ...) |
| horizontal layout | (box :layout 'row ...) |
| flex container | (box :layout 'flex ...) |
| grid container | (box :layout 'grid ...) |
inline-flex 是两个属性的正交组合:
(box :outer 'inline :layout 'flex ...)
Component call 自身没有固定的 :outer 或 :layout;它的根 Text/Box 输出决定
它如何参与父级布局。透明 Component 可以输出多个兄弟节点,因此不能假定每个
Component 都有一个外部盒子。
1.3 属性分组
Box 的属性按 owner 分组,不能做成一个无约束的大 plist:
| 分组 | 属性 | 作用 |
|---|---|---|
| 外部/内部结构 | :outer、:layout、:key |
参与方式、子布局、稳定 identity |
| 几何和表面 | :width、:height、min/max、:margin、:padding、:border、:box-sizing、:overflow |
尺寸、表面和可见区域 |
| 文本 | :face、:color、:bgcolor、:wrap-mode、:text-align |
Text 或普通内容的排版/绘制 |
| Flex 子项 | :order、:flex-grow、:flex-shrink、:flex-basis、:align-self |
只在 Flex 父级中有效 |
| Grid 子项 | :grid-row、:grid-column、span |
只在 Grid 父级中有效 |
| 语义和交互 | :class、:id、:role、:ref、:aria-*、:on-*、:use |
Runtime 事件、Behavior 和查询 |
:outer 和 :layout 是结构/几何属性,变化会触发布局 owner;它们不是 TP
paint slot。ECSS 可以参与解析它们,但最终合法性和布局仍由 Ebox 契约验证。
1.4 子节点与后端 content
目标 ETAF 语法只保留一个内容模型:子节点。
(box "this is text")
等价于:
(box (text "this is text"))
需要文本属性时显式写 text:
(box
:padding '(1 2)
(text :face 'bold "Title"))
ETAF 公共语法不增加 (box :content "...")。:content 是 Ebox 后端构造器
(例如 ebox-create)的内部字段;Renderer 可以把 text 降低为后端 content,
但不能把后端字段反向暴露成第二套 ETAF 子树模型。这样可以自然混合字符串、
Text、Box、Component 和 slot,也不会产生 content 与 children 的优先级歧义。
ETAF 公共模型也不定义 spacer。没有子节点的 Box 就是空 Box;Ebox 可以在
内部使用空内容节点或私有 helper 表示它,但这不是用户需要学习的独立视觉类型。
2. 模块职责
下面的边界是“谁拥有完整规则”的判断标准。调用别的模块的公开契约不等于拥有 那个模块的职责。
2.1 ETAF Core
etaf-view
拥有:
Text、Box、Component call、Fragment 的结构语法和规范化 View 值;- 属性优先解析和
expr :value求值边界; - props、
key和 slot 的静态形状验证; - Component 注册表与 View 名称解析。
不拥有:
- 字宽、像素、布局、滚动和 buffer 写入;
- Component 生命周期和 Runtime 调度;
- 业务数据、数据库、UI 控件实现。
etaf-component
拥有:
etaf-define-component;:view、:setup、:styles定义边界;- props 和静态 Component style 的声明验证。
不拥有:
- Ebox 私有节点;
- Data 请求、数据库连接或全局 UI 控件目录。
etaf-runtime
拥有:
- Component identity、
:key、retained instance; - ref、computed、effect、watch 和 Scope cleanup;
- Context、Theme、Action、Behavior、Data、Resource 的生命周期;
- 依赖失效、候选 generation、提交、回滚和调度;
- 将变化定位到 Component、Range、Host 属性或 Paint contribution。
不拥有:
- 字体测量、flex/grid 算法、像素布局;
- Ebox 私有状态;
- SQLite、HTTP 或某个示例的业务规则。
etaf-renderer
拥有:
- 唯一的 ETAF View → Ebox 公共 lowering 边界;
- Text、Box、Component 输出到 Ebox 公共构造器的转换;
- 把已解析的 style/theme/property contribution 传给下游;
- 明确的
raw-ebox低层出口。
不拥有:
- Component setup/lifecycle 的所有权;
- Ebox 的测量和布局实现;
- Data 查询或应用 Action。
2.2 Ebox
Ebox 是布局和渲染引擎,不是 ETAF Component Runtime。
拥有:
- 文本测量、字宽、换行和内容高度;
- Text 内容测量和换行,以及 row、column、flex、grid 的几何算法;
- padding、border、surface、overflow、scroll 和 viewport 几何;
- 稳定的布局快照、节点 identity、增量几何更新;
- 从布局结果到后端可提交 Render/Paint plan 的转换。
不拥有:
- Component、props、slot、Context、Action、Behavior、Data、Resource;
- 应用语义或 UI 控件分类;
- ETAF 的响应式依赖图。
row、column、flex、grid 即使在 ETAF 目标 API 中统一为
box :layout ...,Ebox 内部仍然可以保留不同的布局上下文和算法。统一的是
上层表达,不是把不同几何问题硬塞进一个无差别函数。
逻辑上可以继续拆为两个边界:
ebox-core 纯测量、布局、快照和 Render plan
ebox-emacs 字体/窗口能力、Ebox Node 到 Emacs buffer 的提交适配
这两个边界不一定现在就变成两个发行包,但不能让布局算法和 Emacs buffer 副作用 混在同一个职责中。
2.3 ECSS
拥有:
- selector 解析和匹配;
- style declaration、cascade、继承和 computed style;
- 把静态规则转换为可复用、可验证的样式中间产物。
不拥有:
- Component state、slot、Action;
- 字宽、布局或 Ebox Node;
- Emacs text property 的提交和回滚。
ECSS 决定“某个结构节点应该得到哪些样式值”,不决定“这些像素如何排布”。
2.4 TP
拥有:
- Emacs text property / paint slot 的分层;
- Theme、Component、state、inline 等 Paint contribution 的优先级合并;
- 原子应用、journal、rollback 和已提交层的复用。
不拥有:
- CSS selector/cascade;
- 布局测量、祖先证明或文字换行;
- Component 和 Data。
TP 应接收精确的 Paint operations,不应该为了换一个背景色重新推导整棵 View 或重新执行所有 Component。
2.5 etaf-ui
拥有官方可复用 Component,例如:
- Button;
- Checkbox;
- Label;
- Panel;
- Number input;
- DataGrid;
- Pagination。
这些控件使用 ETAF 的 props、slot、事件、Behavior、Theme 和 Data 契约。它们 不是第二套 Widget/Control/Component 模型,也不应该实现自己的 Runtime 或布局 引擎。
2.6 etaf-sqlite
拥有:
- SQLite 连接、schema、查询、分页和 mutation;
- 参数和标识符校验;
- 具体 Data Source 的事务和 dispose 边界。
不拥有:
- ETAF Data Controller 状态机;
- View、Component、DataGrid 或 playground UI。
其他数据库、REST、文件和 ORM 集成也应实现同一个具体 Data Source capability,
但使用各自明确的包名,不引入一个笼统的 etaf-adapters 层。
2.7 性能工具
性能记录器必须是应用无关的逻辑模块(当前可以先作为 ETAF 中的独立子模块, 未来再决定是否单独发行)。它拥有:
- operation、stage、父子关系和 generation 记录;
- p50/p95/max、GC、环境信息和跨包阶段关联;
- 公共 Event、Action、mount、flush、Data、Resource、viewport 边界的探针;
- 报告、复制和导出。
它不拥有:
- Research Shelf 或任何示例名称;
- 应用状态或布局规则;
- 为了采样而改变正常渲染路径。
Playground 只能展示这个工具,不能定义它的核心协议。
2.8 两个 Playground
ebox-playground:
- 只验证 Ebox 的布局、测量、重排和性能;
- 只依赖 Ebox 公共 API;
- 不加载 ETAF、Component、Data Controller 或 UI 目录。
etaf-playground:
- 验证 ETAF 的 Component、Runtime、Data、UI 和交互组合;
- 可以选择加载
etaf-ui和etaf-sqlite; - 提供
.etaf示例加载、快捷键和状态展示; - 不拥有 ETAF 的核心语法、Runtime 或性能记录协议。
.etaf 静态文件如果只是 Playground manifest,就属于 Playground;它不是 ETAF
Core 的 Runtime 入口。未来若增加通用 .etaf compiler,compiler 必须把结果
降低到同一套 Text/Box/Component 契约,不能创建第二套 Runtime。
3. 依赖方向
目标依赖图如下:
┌──────────────┐
│ etaf-performance │
│ optional observer│
└──────┬───────┘
│ public probes only
▼
┌────────────┐ ┌────────────────────┐ ┌──────────────┐
│ etaf-ui │──────▶│ ETAF Core │──────▶│ Ebox public │
└────────────┘ │ View/Component/RT │ │ layout port │
└─────────┬──────────┘ └──────┬───────┘
│ │
┌─────────▼─────────┐ ┌──────▼───────┐
│ ECSS / TP adapters│ │ Ebox backend │
└───────────────────┘ │ / Emacs │
└──────────────┘
etaf-sqlite ─────▶ ETAF Data Source contract
etaf-playground ─▶ ETAF + optional etaf-ui + optional etaf-sqlite
ebox-playground ─▶ Ebox public API only
约束:
- Ebox 不向上依赖 ETAF;
- TP 不解析 View,也不调用 Component;
- ECSS 不写 buffer;
etaf-ui不调用 Ebox 私有函数;etaf-sqlite不知道任何 UI;- Playground 不向 core 反向注入示例协议;
- 性能工具通过公开边界观察各包,不让被测包依赖某个示例。
4. Rust 与 Elisp 的切分
目标不是把所有代码机械地翻译成 Rust,而是把确定性计算和环境副作用分开。
适合 Rust 纯计算核心的内容:
- Text/Box Render IR 的验证和规范化;
- keyed diff、identity matching 和 patch batch;
- ECSS selector/cascade/computed-style 计算;
- 字符宽度、换行、普通 Box/flex/grid 测量和布局;
- geometry snapshot、layout impact 和 paint contribution merge。
保留在 Elisp/Emacs 适配层的内容:
- 执行用户定义的 Component 函数;
- ref、Context、Action、Data、Resource 和 lifecycle callback;
- Emacs 事件、窗口、字体能力和 buffer 提交;
- 调用 Rust 核心并提交返回的 patch/paint plan。
Elisp 不应再次实现 Rust 已经计算过的布局、cascade、diff 或文字扫描;Rust 也不 应读取 Emacs buffer 或理解 Component slot 语义。
5. 性能与正确性不变量
模块增加不能把一次局部变化升级成多次全量遍历。必须保持:
- 一次事务中的稳定 identity、依赖、布局快照和 Paint contribution 只 materialize 一次;
- Text paint 改变不重新执行无关 Component,不重新做布局;
- Box 几何改变只触发受影响的布局 owner;
- Component 状态/slot/列表拓扑改变只执行拥有该结构的 Component;
- Ebox 失败或 TP 提交失败时保留上一代已提交结果;
- 快速路径必须是普通正确路径的严格子集,并且能验证失败后精确回退;
- 性能工具本身不能改变被测应用的调度和布局路径。
真实 GUI 场景仍然是最终证据。固定场景在功能、文本属性、identity、生命周期和 回滚语义不缩水的前提下,p95 和 max 都必须满足项目的 50ms 目标;单独的微基准 不能替代跨包验证。
6. 当前实现与目标模型的关系
当前 ETAF Core 的 Host 注册表仍包含:
text · fragment · container · row · column · stack · flex · grid · spacer
当前 Renderer 直接把这些名称映射到 Ebox 构造器;Ebox Playground 也直接使用
自己的布局 API。这只是现状记录,不是目标 API,也不构成必须保留的兼容表面。
spacer 和 flow 都不属于目标公共词汇;目标 Ebox 可以用普通 Box/Text 路径
替代当前的 flow 标记,不为它保留独立布局分支。
目标演进方向是:
Text + Box(layout)
↑
如果确实需要短写法,row/column/flex/grid 只能作为新语法糖
这些简写不能通过 etaf-define-component 实现,否则每个布局节点都会被迫获得
Component 的 identity、scope、slot 和调度边界。它们应直接降低为 Ebox 布局节点。
不保留 container、stack、spacer 或旧 Host 注册名的兼容别名。
目标模型尚未交付前,不得修改现有 Host 语法后再把未验证的 box 描述成已完成。
7. 有边界的演进顺序
如果开始实现目标模型,只做下面这个最小顺序:
- 增加
Box的规范化 View 形状和字符串子节点规则; - 只验证
layout='column的box到 Ebox lowering; - 决定是否提供
row、column、flex、grid的新语法糖,并直接降低到 Box; - 分别验证
flex和grid的模式属性、布局结果和增量路径; - 保留旧名称兼容,并用 Research Shelf 与 Flex reference 做 GUI 回归;
- 只有全部公共测试和真实性能门禁通过,才更新用户指南为
text + box。
任何一步不能证明结果等价或性能收益,就删除该实验,不继续堆下一层抽象。
这是一次干净重设计,不要求旧 .el、.etaf 文件或旧 Host 名称继续运行。迁移
由明确的源码更新完成,不通过长期兼容层隐藏模型变化。
8. 明确不做的事情
- 不把
button、panel、data-grid等业务控件塞进 Ebox; - 不把
row、column、flex、grid做成 Runtime Component; - 不把
spacer或flow暴露成 ETAF 公共视觉类型; - 不在 ETAF 公共层引入
box :content第二套内容模型; - 不让 TP 重新扫描布局或重新调和所有颜色;
- 不让 Playground 拥有通用框架协议;
- 不为了“预编译”而新增没有长编译时间和真实运行时收益的中间产物;
- 不为了减少名词而合并 Component、Text、Box、Ebox Node、Paint slot 等不同所有权 和生命周期概念。