etaf/docs/architecture.zh.md
Kinneyzhang 0185c4e05a feat: establish unified etaf view foundation
Implement the P0 View grammar, expr bridge, stateless view Components, and Ebox mount path in a new independent package. Include bilingual architecture and implementation documents plus contract tests.
2026-08-05 00:36:52 +08:00

6.4 KiB
Raw Blame History

ETAF 架构

本文定义目标架构,是公共概念和职责边界的契约,不是变更日志。

1. 唯一的降级路径

Application
  → Runtime / State / Action / Data
  → Component(props, local scope)
  → View
  → ETAF Renderer
  → Ebox Node
  → measure → layout → paint → commit
  → Emacs buffer

ETAF 描述应用结构和行为Ebox 是底层引擎,负责把可测量的 Node 树转换为文本 surface并原子地发布这些 surface。每个新的 ETAF 功能都必须在这条路径上拥有唯一职责归属。

2. 用户需要理解的四个概念

View

View 是结果的结构描述。View 可以是 Host 调用、Component 调用、字符串文本叶子、nil,或者这些值组成的序列。

Component

Component 是复用边界。它接收 props可选地拥有局部 Scope并产生 View。Component 不是另一种 Ebox Node也不应因为“可复用”就增加新的 renderer 分支。

Node

Node 是 Ebox 的可测量、可渲染模型。Ebox 负责几何、盒模型、布局、surface 属性、滚动、运行时身份和 buffer 发布。ETAF 不把 Component 语义、slot、Action 或事件塞进 Ebox 属性。

Runtime

Runtime 负责挂载、调度、状态失效、事件边界、提交协调、回滚和销毁。Runtime 是应用边界,不是另一种 View 节点。

3. 统一的 View 语法

所有 Host 和 Component 调用都使用同一种形状:

(NAME ATTRIBUTE* CHILD*)

属性是 :KEY VALUE;子节点是 View、字符串、nil,或通过 expr 产生的序列。属性区必须在子节点区之前完整结束:

(text :face 'bold :color "#F4F6FB" "Hello")

下面的写法非法,因为属性区和子节点区交错:

(text "Hello" :face 'bold)

核心 Host 和公共 Component 都遵循同一规则;text 没有另一套“位置参数作为内容”的特殊约定。

4. Elisp 求值边界

编译器读取结构位置中的 View不要求对 View 使用 quote

(etaf-view (text "Hello"))

属性值是普通 Elisp 表达式位置:

(etaf-view
 (text :face (if dark 'light 'dark) "Theme"))

子节点区只有一个明确的计算桥接:

(expr :value (if checked "☑" "☐"))

expr 只接受 :valueifwhencondletmapcar 及其他 Elisp 形式仍然是该 value 内的普通 ElispETAF 不增加单独的 if、循环或计算节点类别。

表达式要返回 View 时,显式构造它:

(expr
 :value
 (when open
   (etaf-view (text :face 'bold "Details"))))

'(text "Details") 是数据,不是 View。因此 quote 既不是全局禁止,也不是第二套 View 编译器。

5. Component

公共定义模型只有入门形式和高级形式:

(etaf-define-component NAME (&key PROPS)
  :view VIEW)

(etaf-define-component NAME (&key PROPS)
  :setup SETUP)

:view 是结构语法,不需要 quote:setup 是普通 Elisp用于建立 Component Scope 并返回零参数 render 函数;普通 Elisp 产生 View 时使用 etaf-view:view:setup 互斥。组件静态样式使用独立的定义元数据入口 :styles (styles RULE...)styles 不是第三种渲染模型。

Props 和 children 的职责不同。Props 是 Component 声明的命名输入;末尾 children 和具名 slot 组成同一个 slot collection而不是隐藏的第二套 Component 参数表。该 collection 在 Component 边界统一归一化并投影。

在 View 语法中,公共 Component 可以省略 etaf- 前缀;普通 Elisp APIetaf-valueetaf-refetaf-mount,保留前缀。别名解析只发生在结构位置;发生冲突时使用 list-view 这样的语义别名,不覆盖 Elisp 的 list

6. 核心 Host 与官方 Component

ETAF 核心只包含最小的、无样式 Host

text · fragment · container · row · column · stack · flex · spacer

这些名字表达结构布局和文本 surface。Button、Checkbox、Input、Dialog、DataGrid 等产品级控件都是独立 etaf-ui 包提供的普通 Component。核心的可按压能力通过 :role:on-press 等语义属性表达,不建立与 etaf-ui Button 竞争的核心 Button Host。

字符串是最小文本 View会降级为带 content 的 Ebox boxtext 是用于样式和语义属性的显式文本 Host不是另一种 Ebox Node 家族。

7. 非视觉能力

这些机制扩展同一个 Component/Host 边界,但不建立平行的视觉节点类别:

机制 职责 入口
on-xx 一个局部事件回调 Host/Component 属性
Action 有名字、可测试的业务变更 Action 定义和 dispatch
Effect 外部同步、订阅和清理 Scope 中的 Effect
Behavior 可复用的非视觉能力集合 :use 属性
watch 响应式失效原语 Reactive API 或 Behavior
Context 继承的 Scope 值 Provide / Inject
Data 请求、状态、取消和错误归属 Core data API

expr 负责计算子节点值不拥有身份、生命周期、订阅、buffer 写入或 Effect。Behavior 可以使用事件、ref、Effect 和 watch但自身不会变成 View 节点。

8. 包职责

ebox
  └── 可选的 ebox-playground

etaf → ebox
  ├── reactive / state / action / effect / data / ECSS
  └── 纯 .etaf compiler

etaf-ui → etaf
etaf-playground → etaf展示官方 Component 时再依赖 etaf-ui

Ebox 不理解 Component、slot、Action、Context 或 data。ETAF 只通过 renderer 边界和 Ebox 公共 API 使用 Ebox。etaf-data 是 core 能力,不是用户必须额外安装的平级包。数据库和外部集成使用明确的数据源包,例如 etaf-sqlite;不需要抽象的公共 etaf-adapters 层。

两个 Playground 都是独立的可选工具:ebox-playground 只使用 Eboxetaf-playground 使用 ETAF 公共 API绝不调用 Ebox Playground 或 Ebox 私有函数。

9. 扩展规则

应用层功能优先使用 Component、Behavior、Action、Effect、Context 值或 data source。只有当功能确实改变测量、布局、surface 绘制、滚动或发布,并且已经定义好 signature、dirty 分类、渲染、回滚和测试责任时,才增加 Ebox 属性或 Node。这样既保持用户模型小而统一又通过普通表达式位置和 Runtime API 保留 Elisp 的图灵完备能力。