783 lines
33 KiB
Markdown
783 lines
33 KiB
Markdown
# ETAF 用户使用指南
|
||
|
||
ETAF 使用一套很小的词汇构建文本应用:`View`、`Component`、props、children、响应式状态和 Ebox 渲染。先学习 `etaf-view` 与 `etaf-mount`;只有 Component 需要局部状态或生命周期时才增加 `:setup`。
|
||
|
||
## 1. 安装与加载
|
||
|
||
ETAF 依赖独立的 Ebox 包。开发时把核心检出目录放入 `load-path`,然后只加载 ETAF 的公共入口:
|
||
|
||
```elisp
|
||
(add-to-list 'load-path "/path/to/github/ecss")
|
||
(add-to-list 'load-path "/path/to/github/tp")
|
||
(add-to-list 'load-path "/path/to/github/ebox")
|
||
(add-to-list 'load-path "/path/to/github/etaf")
|
||
(require 'etaf)
|
||
```
|
||
|
||
这个入口会加载核心 View、Component、Runtime、reactive、Context、Data、Resource、事件、Behavior 和 Action API。`etaf-ui`、`etaf-sqlite` 和两个 Playground 都是独立的可选包;加载 `etaf` 不会自动加载它们。
|
||
|
||
## 2. 第一个 View
|
||
|
||
所有结构都使用:
|
||
|
||
```elisp
|
||
(NAME :property value ... child ...)
|
||
```
|
||
|
||
属性在前,子节点在后。`etaf-view` 接收不带 quote 的结构 form:
|
||
|
||
```elisp
|
||
(etaf-view
|
||
(column
|
||
(text :font-weight 'bold "Hello")
|
||
(text :color "#687386" "Welcome to ETAF")))
|
||
```
|
||
|
||
把它挂载到 Emacs buffer:
|
||
|
||
```elisp
|
||
(etaf-mount
|
||
"*etaf-hello*"
|
||
(etaf-view
|
||
(column
|
||
(text :font-weight 'bold "Hello")
|
||
(text "This is a text application."))))
|
||
```
|
||
|
||
`etaf-mount` 返回 buffer。同一个调用会先释放该 buffer 中已有的 Runtime,再挂载新的 View。需要显式释放时:
|
||
|
||
```elisp
|
||
(etaf-unmount (etaf-runtime-for-buffer "*etaf-hello*"))
|
||
```
|
||
|
||
Headless host 或已经知道最终布局上下文的调用方,可以通过可选第三个参数传入
|
||
初始 viewport。这样第一次 Ebox publication 会直接使用最终尺寸,不需要紧接着
|
||
再 rerender:
|
||
|
||
```elisp
|
||
(etaf-mount buffer view
|
||
'(:viewport-width 1200 :viewport-height 48))
|
||
```
|
||
|
||
纯渲染或测试可以使用 `etaf-render`:
|
||
|
||
```elisp
|
||
(ebox-render
|
||
(etaf-render
|
||
(etaf-view (text :font-weight 'bold "Pure View"))))
|
||
```
|
||
|
||
View 含有状态型 Component、响应式数据、事件或生命周期时,使用 `etaf-mount`。
|
||
|
||
需要显式请求处理待办更新时,调用 `(etaf-runtime-flush runtime)`。
|
||
它现在返回 **Ebox 已提交 revision 整数**,不再返回旧版的 Ebox 节点。
|
||
Runtime 忙碌或仍处于批处理时,更新可能继续等待;返回值标识此刻读者可见的
|
||
发布版本。活动 Ebox/TP 事务内的调用会在请求更新之前报错,因为事务中的
|
||
revision 可能尚未提交。普通 flush 不导出整棵树,也不强制重建 Root。
|
||
|
||
需要当前树及其配套 source facts 时,显式获取快照:
|
||
|
||
```elisp
|
||
(let* ((runtime (etaf-runtime-for-buffer "*etaf-hello*"))
|
||
(snapshot (etaf-runtime-snapshot runtime)))
|
||
(list (plist-get snapshot :revision)
|
||
(plist-get snapshot :mount-id)
|
||
(ebox-render (plist-get snapshot :input))))
|
||
```
|
||
|
||
快照包含 `:input`(canonical Ebox input)、`:revision` 和 `:mount-id`
|
||
(本次挂载的身份)。导出成本为 O(N),会分离节点的普通可变数据;它不处理
|
||
待办更新,不求值 Component,不发布,也不增加 revision。input 在后续提交或
|
||
卸载后仍可使用。callback 等不透明能力保持身份,查询不会冻结这些外部能力或
|
||
显示环境。对未挂载 Runtime 或在活动 Ebox/TP 事务内查询会报错。
|
||
|
||
已废弃的 `etaf-runtime-root-node` getter 通过上述 O(N) 查询保留读取兼容性,
|
||
返回当前唯一根节点;Runtime 不再保存根节点镜像。请迁移到
|
||
`etaf-runtime-snapshot`,保留 canonical input 中与根节点配套的 source facts;
|
||
不要再把旧 getter 当作低成本字段读取或写入目标。
|
||
|
||
当前 core 不直接加载 `.etaf`。`etaf-define-component` 是结构/样式/行为单元:用 View 定义结构,用 `:styles` 放静态 presentation,用 `:setup` 管理 retained state、Action 和生命周期。未来 `.etaf` SFC 属于把结果编译成同一套 Component 契约的 compiler layer,而不是第二个 Runtime 入口。
|
||
|
||
## 3. 属性与子节点
|
||
|
||
属性值就是普通 Elisp 表达式,不需要额外的 `expr`:
|
||
|
||
```elisp
|
||
(let ((dark t)
|
||
(label "Theme"))
|
||
(etaf-view
|
||
(text
|
||
:color (if dark "#F4F6FB" "#1F2328")
|
||
:background-color "#20242B"
|
||
(expr label))))
|
||
```
|
||
|
||
子节点区是结构语法。`expr` 是执行普通 Elisp 的唯一明确桥接:
|
||
|
||
```elisp
|
||
(etaf-view
|
||
(column
|
||
(expr (if loading "Loading..." "Ready"))
|
||
(expr
|
||
(when open
|
||
(etaf-view (text :font-style 'italic "Details"))))))
|
||
```
|
||
|
||
`expr` 只接受一个普通 Elisp form,不能有结构子节点。在结构子节点位置,返回值
|
||
可以是字符串、typed Host 或 Component View、这些值组成的 proper sequence 或 `nil`。
|
||
在 `text` Host 内,表达式必须返回字符串。`if`、`when`、`cond`、`let`、`mapcar` 和
|
||
`cl-loop` 仍是普通 Elisp。
|
||
|
||
quote 只有普通 Elisp 的含义:
|
||
|
||
- 结构性 View form 不要 quote。
|
||
- 字面量 symbol 和数据列表在 Elisp 需要时才 quote。
|
||
- Elisp 表达式需要构造 View 时,在其中使用 `(etaf-view ...)`。
|
||
|
||
例如 `'bold` 是 `:font-weight` 的 symbol 值,而 `'(text "data")` 只是数据,不会渲染。动态 View 必须写成 `(etaf-view (text "data"))`。
|
||
|
||
间距由布局 Host 决定:`row` 与 `column` 使用 `:item-gap`,例如
|
||
`(row :item-gap 1 ...)`;`flex` 与 `grid` 使用 `:gap`。切换 Host 时也要选择
|
||
对应的间距属性,两者不是可互换的 alias。例如将 `(row :gap 1 ...)` 改为
|
||
`(row :item-gap 1 ...)`。
|
||
|
||
核心 `grid` Host 用于二维布局:
|
||
|
||
```elisp
|
||
(etaf-mount
|
||
"*etaf-grid*"
|
||
(etaf-view
|
||
(grid
|
||
:width '(640)
|
||
:grid-template-columns '((200) (fr 1))
|
||
:grid-template-rows '(1 1)
|
||
:gap '(1 (12))
|
||
(text :font-weight 'bold "Name")
|
||
(text "Value")
|
||
(text "Ada")
|
||
(text "Lovelace"))))
|
||
```
|
||
|
||
轨道模板支持 `auto`、`min-content`、`max-content`、`(fr FACTOR)`、`(minmax MIN MAX)` 和 `(repeat COUNT TRACK-LIST)`。`:grid-auto-columns` 与 `:grid-auto-rows` 负责隐式轨道尺寸;`:grid-auto-flow` 支持 `row` 或 `column`。子节点可以使用 `:grid-column`、`:grid-row`、`:grid-column-span` 和 `:grid-row-span`;测量、放置以及 item/content 对齐由 Ebox 完成。可选 native backend 不支持 Grid 时会回退到 Elisp Ebox renderer。
|
||
|
||
## 4. 定义 Component
|
||
|
||
入门形式是无状态的 `:view` Component:
|
||
|
||
```elisp
|
||
(etaf-define-component status-label (&key label)
|
||
"Render a status label."
|
||
:view
|
||
(text
|
||
:font-weight 'bold
|
||
(expr label)))
|
||
|
||
(etaf-mount
|
||
"*etaf-status*"
|
||
(etaf-view
|
||
(status-label :label "Connected")))
|
||
```
|
||
|
||
调用时使用传给 `etaf-define-component` 的准确名称:
|
||
|
||
```elisp
|
||
(etaf-view (status-label :label "Connected"))
|
||
```
|
||
|
||
注册表不会自动生成 alias。定义为 `etaf-status-label` 的组件必须使用这个准确名称;上面的 `status-label` 是本节自己显式定义的名称。加载 `etaf-ui` 后,官方目录使用 `etaf-button`、`etaf-checkbox` 等准确名称。
|
||
|
||
定义宏只接受这些关键字:
|
||
|
||
| 关键字 | 作用 |
|
||
| --- | --- |
|
||
| `:view` | 声明式 View frontend,与 `:render` 互斥 |
|
||
| `:render` | 普通 Elisp 返回一个 typed View,通常使用 `etaf-view`;程序化构造也可使用 `etaf-node` |
|
||
| `:setup` | 可选的一次性初始化,返回由 `etaf-state` 读取的 opaque 状态 |
|
||
| `:styles` | 可选的静态作用域样式声明 |
|
||
|
||
没有单独的 children、slot、event、state 或 variant 声明块。业务 props 通过 `(&key ...)` 声明,children 和 slot 是隐式内容。
|
||
|
||
## 5. 局部状态与生命周期
|
||
|
||
Component 自己拥有状态时使用 `:setup`:
|
||
|
||
```elisp
|
||
(etaf-define-component counter (&key title)
|
||
"Render a retained counter."
|
||
:setup
|
||
(let ((count (etaf-ref 0))
|
||
(initial-title title))
|
||
(etaf-on-mounted
|
||
(lambda () (message "%s mounted" initial-title)))
|
||
(etaf-on-unmounted
|
||
(lambda () (message "%s unmounted" initial-title)))
|
||
count)
|
||
:render
|
||
(let ((count (etaf-state))
|
||
(caption title))
|
||
(etaf-view
|
||
(column
|
||
(text :font-weight 'bold (expr caption))
|
||
(text (expr (format "Count: %d" (etaf-value count))))
|
||
(text :role 'button :tab-index 0
|
||
:on-press (lambda () (cl-incf (etaf-value count)))
|
||
"Increment")))))
|
||
```
|
||
|
||
Setup 对 retained instance 只执行一次,返回一个 opaque 状态值。选定的 `:view` 或
|
||
`:render` frontend 在更新时运行,并通过 `etaf-state` 读取这个准确值。
|
||
`etaf-on-mounted`、`etaf-on-updated` 和 `etaf-on-unmounted` 注册该 Component 的生命周期
|
||
callback。Scope 释放时会自动停止响应式 effect 并运行 cleanup。
|
||
|
||
`:view` 和 `:render` 共用编译、slot 投影与 prop 校验。用普通 `let`/`let*`
|
||
为回调捕获 state 句柄或当前 prop 值;`etaf-state` 只在 render 时读取,不留到
|
||
事件触发时调用。把 `etaf-value` 保留在需要更新的属性或 `expr` 内;提取句柄
|
||
不需要提前读取它的值。含有持久闭包的组件代码放在启用 lexical-binding 的 `.el`
|
||
文件中。简单本地回调不需要定义 Action。
|
||
|
||
响应式 API 只有一套模型:
|
||
|
||
```elisp
|
||
(let* ((count (etaf-ref 0))
|
||
(double (etaf-computed
|
||
(lambda () (* 2 (etaf-value count))))))
|
||
(etaf-watch count
|
||
(lambda (new old)
|
||
(message "%s → %s" old new)))
|
||
(setf (etaf-value count) 1)
|
||
(etaf-value double))
|
||
```
|
||
|
||
如果函数形式更清楚,可以使用 `etaf-set-value`。`etaf-watch-effect` 用于响应式副作用,并且可以返回 cleanup:
|
||
|
||
```elisp
|
||
(etaf-watch-effect
|
||
(lambda ()
|
||
(message "Count is %s" (etaf-value count))
|
||
(lambda () (message "Stop observing count"))))
|
||
```
|
||
|
||
在 Component setup 中,effect 和 watch 属于 Component Scope。在 Component 外部,需要用 `etaf-effect-scope` 和 `etaf-scope-run` 明确创建 Scope。
|
||
|
||
## 6. children 与 slot
|
||
|
||
普通尾部子节点就是匿名/默认 slot:
|
||
|
||
```elisp
|
||
(etaf-define-component panel (&key title)
|
||
"Render a titled panel."
|
||
:view
|
||
(column
|
||
(text :font-weight 'bold (expr title))
|
||
(slot (text :color "#687386" "No content"))))
|
||
|
||
(etaf-view
|
||
(panel
|
||
:title "Account"
|
||
(text "Account body")))
|
||
```
|
||
|
||
命名 slot 使用 `:name`,并且必须是稳定的、非 keyword 的 symbol:
|
||
|
||
```elisp
|
||
(etaf-define-component card (&key title)
|
||
"Render a card with a header slot."
|
||
:view
|
||
(column
|
||
(slot :name 'header
|
||
(text :font-weight 'bold (expr title)))
|
||
(slot (text :color "#687386" "No body"))))
|
||
|
||
(etaf-view
|
||
(card
|
||
:title "Account"
|
||
(slot :name 'header (text "Account settings"))
|
||
(text "Body")))
|
||
```
|
||
|
||
默认 slot 的两个用户简写是 `(slot)` 和 `(slot FALLBACK...)`。内部统一形式是 `(slot :name 'default FALLBACK...)`。调用处的普通子节点填充 `default`;命名内容写成 `(slot :name 'header CHILD...)`。显式空的 `(slot :name 'header)` 会抑制 fallback。字符串、数字、变量和运行时表达式都不是合法 slot name。
|
||
|
||
Slot 中作者写下的表达式在整个投影子树中保留作者的 props、state 和 Context。
|
||
其中创建的 Component 仍有自己的 props、state、styles 和 Scope,其 Context 从
|
||
slot 作者环境继承。接收 slot 的 Component 不会把自己的 Context 注入调用者内容。
|
||
它自己定义的 fallback、普通子节点,以及产生 View 的 callback 使用接收方环境。
|
||
Table/Grid 的 cell callback 同样使用消费它的 Table/Grid 所在位置的 Context。
|
||
|
||
在结构边界,`expr` 可以返回 typed View 或 typed View 的 proper sequence,但它不暴露、
|
||
也不接受 ETAF 私有 struct。同一个 Component 可以组合 keyed `:for`、结构表达式和
|
||
命名 footer slot:
|
||
|
||
```elisp
|
||
(etaf-define-component etaf-docs-collection-card (&key items footer-view)
|
||
"Render keyed rows, one dynamic typed View, and a footer slot."
|
||
:view
|
||
(column
|
||
(row :for (item items) :key (car item)
|
||
(text (expr (cdr item))))
|
||
(expr footer-view)
|
||
(slot :name 'footer (text "No footer"))))
|
||
```
|
||
|
||
## 7. 样式与 Theme
|
||
|
||
静态 Component 样式只有一种声明形式:
|
||
|
||
```elisp
|
||
(etaf-define-component styled-card ()
|
||
"Render a small styled card."
|
||
:styles
|
||
(styles
|
||
("&"
|
||
:padding (1 2)
|
||
:border (1 solid "#687386"))
|
||
(".title" :font-weight bold)
|
||
(".danger" :color "#FF6B6B"))
|
||
:view
|
||
(column
|
||
:class "card"
|
||
(text :class "title" "Title")
|
||
(slot)))
|
||
```
|
||
|
||
外层 `styles` 是静态 Component metadata。每条规则是 `("SELECTOR" :PROPERTY VALUE...)`;`(1 2)` 和 `bold` 在样式中是数据,不需要 quote。普通 View 属性仍遵循 Elisp 规则,字面量列表通常需要 quote。
|
||
|
||
优先级固定为:
|
||
|
||
```text
|
||
显式 Host 属性 > Component :styles > 继承的 Theme 默认值
|
||
```
|
||
|
||
在同一个 Component 样式作用域内,第一条匹配声明会填充尚未指定的 Host
|
||
属性;后续规则不会覆盖已经解析出的默认值。非 nil 的 Host 属性始终受
|
||
保护,nil Host 属性按省略处理,可以接收 Component 或 Theme 默认值。若
|
||
variant 需要明确覆盖,应使用显式 Host 属性或不同的属性键。
|
||
|
||
样式作用域归属于创建 View node 的 Component。父组件规则不会进入嵌套 Component 的内部;调用者通过 slot 提供的内容保留 caller scope,Component 自己定义的 slot fallback 保留 child scope。
|
||
|
||
Theme 是 Context 的便捷形式,不是另一个 Runtime 对象:
|
||
|
||
<!-- etaf-example: theme -->
|
||
```elisp
|
||
(require 'etaf)
|
||
|
||
(etaf-define-component themed-shell ()
|
||
"Provide semantic colors to its own View."
|
||
:setup
|
||
(etaf-theme-provide
|
||
'(:text-color "#F4F6FB" :surface-color "#202634"))
|
||
:view
|
||
(text :ref 'themed-content
|
||
:color (etaf-theme-token :text-color)
|
||
:background-color (etaf-theme-token :surface-color)
|
||
"Themed content"))
|
||
|
||
(etaf-mount "*etaf-theme*" (etaf-view (themed-shell)))
|
||
```
|
||
|
||
如果应用有亮/暗两套 palette,应把语义 role 集中放在一份 palette plist 中,
|
||
再显式调用 `(etaf-theme-resolve-palette palette 'light)` 或 `'dark`。这样应用的
|
||
Theme toggle 不依赖 Emacs frame。可选的 `etaf-theme-tp` adapter 可以把 TP 的
|
||
palette registry 转换为同一份 ETAF Theme contract;`etaf-ui` 和 core Runtime
|
||
都不依赖 TP palette 名称。
|
||
|
||
静态 Component 样式可以用 `(etaf-theme-token :token-key)` 延迟读取继承的
|
||
Theme。ETAF 会在 lowering 样式作用域时解析它,使重复的 retained Host 复用静态
|
||
样式路径,而不是每个 Host 都携带一个动态 inline 属性。
|
||
|
||
## 8. 事件、Action、Behavior 与 focus
|
||
|
||
一次局部事件使用 `:on-*` 属性:
|
||
|
||
```elisp
|
||
(text
|
||
:ref 'save
|
||
:role 'button
|
||
:on-press (lambda () (message "Saved"))
|
||
"Save")
|
||
```
|
||
|
||
Runtime 会按 Host 引用保存 handler。测试和集成可以直接 dispatch:
|
||
|
||
```elisp
|
||
(let ((runtime (etaf-runtime-for-buffer "*etaf-status*")))
|
||
(etaf-dispatch-event runtime 'save 'press))
|
||
```
|
||
|
||
命名的业务变更使用 Action:
|
||
|
||
```elisp
|
||
(etaf-action-define save-record (runtime record)
|
||
"Save RECORD through the application boundary."
|
||
(ignore runtime)
|
||
(message "Saving %S" record))
|
||
|
||
(text
|
||
:role 'button
|
||
:on-press (lambda () (etaf-dispatch 'save-record record))
|
||
"Save")
|
||
```
|
||
|
||
Action 函数第一个参数是 Runtime。`etaf-dispatch` 必须在挂载的 Runtime 中运行,或者把 Runtime 作为第一个参数显式传入。
|
||
|
||
Behavior 用来打包可复用的非视觉属性和 cleanup:
|
||
|
||
```elisp
|
||
(text
|
||
:use (list (etaf-focusable))
|
||
:role 'button
|
||
"Focusable text")
|
||
```
|
||
|
||
使用 `etaf-define-behavior` 定义应用 Behavior;只使用一次的交互不需要定义 Behavior,直接使用 `:on-*`。`etaf-toggleable` 可用于 controlled value。Behavior 不是视觉节点,也不直接编辑 buffer。
|
||
|
||
如果需要可复用的 installer,可以把产生 cleanup 的部分放到保留的 `:install` 属性中:
|
||
|
||
```elisp
|
||
(etaf-define-behavior traced-focus (&rest attributes)
|
||
"Install a Behavior with a visible lifecycle trace."
|
||
(apply #'etaf-behavior-create
|
||
'traced-focus
|
||
(append attributes
|
||
(list :install
|
||
(lambda ()
|
||
(message "Behavior installed")
|
||
(lambda ()
|
||
(message "Behavior removed")))))))
|
||
```
|
||
|
||
Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。Behavior equality 对 function 和 reactive value 使用 `eq`;因此新建的 installer closure 会被视为有意替换,而不是错误复用。替换状态先以 mounted resource registry 的 staged resource 保存,generation commit 后才成为 authority。
|
||
|
||
<a id="interaction-migration"></a>
|
||
|
||
根事件透传采用追加规则:内部业务 handler 最先运行(first),然后是由内到外
|
||
wrapper 附加的 callback,最后是按声明顺序运行的 Behavior。每个声明位置执行
|
||
一次。Checkbox 的 `:on-change` 仍先收到下一个布尔值,附加的 `:on-press`
|
||
观察回调随后运行。callback 报错会 short-circuit 剩余回调;UI 回滚不会撤销外部
|
||
业务写入。dispatch 只命中准确 Host,不存在 capture 或 bubble。
|
||
|
||
wrapper 的 `:use` 列表顺序连接,重复 Behavior name 在任何 installer 运行前报错。
|
||
非事件 Behavior 默认值仍由 Host 优先、其余 first-wins;`:disabled` 则取 OR。
|
||
每次更新都重新计算内外禁用输入:调用方可进一步禁用控件;解除外层禁用时,只有
|
||
内部也为 nil 才能启用。禁用 Host 的 `etaf-dispatch-event` 和 `etaf-focus` 会抛出
|
||
`etaf-event-error`。禁用时不安装输入 Behavior;提交禁用时清理已安装资源,重新启用
|
||
时再安装。
|
||
|
||
命中测试先选择最深的交互边界,再检查是否启用。点击禁用 cell 按钮不会激活父行,
|
||
即使按钮与行的 bounds 相同。普通非交互行文本仍可选择该行;显式聚焦行后也可触发
|
||
行动作。
|
||
|
||
迁移时注意:附加的根 `:on-*` 现在追加执行,不再覆盖原动作。需要不同业务动作时,
|
||
使用组件显式公开的业务 callback prop,或定义具有该行为的组件。透传属性不能把
|
||
已有的 `:role` 或 `:aria-checked` 等归组件所有的 aria 状态改成冲突值,否则报
|
||
Component 输入错误;有意提供语义变体时应公开业务 prop。调用方仍可覆盖
|
||
`:aria-label` 与 `:aria-description` 的可访问性文字。
|
||
|
||
Action name 使用 application/feature-prefixed symbol;重复 Action 注册默认报错。显式
|
||
reload 用 `etaf-action-redefine-run` 包住替换,它只改变未来按 name 的 dispatch,不会
|
||
flush 已挂载 Runtime。
|
||
|
||
Focus 和 hit testing 是 Runtime 操作:
|
||
|
||
```elisp
|
||
(let ((runtime (etaf-runtime-for-buffer "*etaf-status*")))
|
||
(etaf-focus-next runtime)
|
||
(etaf-activate runtime))
|
||
```
|
||
|
||
`etaf-host-ref-bounds` 和 `etaf-host-ref-position` 暴露公共的 Ebox hit-test 边界。`etaf-dispatch-event` 支持可选 payload 标记,用于给 callback 传入一个参数。
|
||
|
||
挂载后的 buffer 会自动启用 `etaf-input-mode`:`TAB` 聚焦下一个 Host,`Shift-TAB`/backtab 聚焦上一个 Host,`RET` 激活当前焦点 Host,`mouse-1` 在点击位置激活 Host。焦点顺序先按数字 `:tab-index`,相同 index 再按 live buffer position 稳定排序;移动焦点也会移动 point。卸载时会禁用该输入 mode。
|
||
|
||
## 9. Context / Provide / Inject
|
||
|
||
Context 适合跨多层共享依赖,不适合普通 label:
|
||
|
||
<!-- etaf-example: context -->
|
||
```elisp
|
||
(require 'etaf)
|
||
|
||
(etaf-define-component service-label ()
|
||
"Read the inherited service."
|
||
:setup (etaf-inject 'service nil t)
|
||
:view
|
||
(text (expr (format "Service: %s" (etaf-value (etaf-state))))))
|
||
|
||
(etaf-define-component application-shell ()
|
||
"Provide a service to its own child Component."
|
||
:setup
|
||
(let ((service (etaf-ref "demo-service")))
|
||
(etaf-provide 'service service)
|
||
service)
|
||
:view (service-label))
|
||
|
||
(etaf-mount
|
||
"*etaf-context*"
|
||
(etaf-view (application-shell)))
|
||
```
|
||
|
||
Context key 是稳定的普通 symbol,最近的祖先优先。`etaf-inject` 对可选依赖返回 default,对必需但缺失的依赖触发 `etaf-context-error`。注入的 ref 或 computed 保留自身响应式 identity。
|
||
|
||
迁移:在根位置编写的 slot 内容保留根的空 Context,其中的嵌套 Component 也
|
||
不会意外接收 slot 接收方的 provider 或 Theme。若消费者需要 provider 的
|
||
Context,应像上例一样写在 provider 自己的 View 中;需要外部定制时,可接收
|
||
普通的 View-producing callback 并在该位置调用。内容需要保留作者 Context 时
|
||
使用 slot。
|
||
|
||
## 10. Data Controller 与 DataGrid
|
||
|
||
Data 已经是 ETAF core 能力。Data Source 实现一个小的 source 契约:
|
||
|
||
```elisp
|
||
(setq source
|
||
(etaf-data-source
|
||
:load (lambda (query page page-size)
|
||
(ignore query)
|
||
(let ((rows '((:id 1 :name "Ada")
|
||
(:id 2 :name "Grace"))))
|
||
(list :items rows
|
||
:total (length rows)
|
||
:page page
|
||
:page-size page-size)))
|
||
:mutate (lambda (operation payload)
|
||
(ignore operation payload)
|
||
t)
|
||
:dispose (lambda () nil)))
|
||
```
|
||
|
||
官方 UI Component 使用和用户 Component 相同的 controlled-prop 模型:
|
||
|
||
```elisp
|
||
(require 'etaf-ui)
|
||
|
||
(let ((done (etaf-ref nil)))
|
||
(etaf-mount
|
||
"*etaf-checkbox*"
|
||
(etaf-view
|
||
(etaf-checkbox
|
||
:checked (etaf-value done)
|
||
:label "Done"
|
||
:on-change (lambda (next)
|
||
(setf (etaf-value done) next))))))
|
||
```
|
||
|
||
Component 发出 next value;ref 由调用方拥有,并在下一次 render 提供当前值。
|
||
|
||
`:load` 接收 query 和分页参数,返回带 `:items` 的 plist;`:mutate` 和 `:dispose` 可选。内置 memory source 适合本地示例:
|
||
|
||
```elisp
|
||
(setq source
|
||
(etaf-data-memory-source
|
||
'((:id 1 :name "Ada")
|
||
(:id 2 :name "Grace"))
|
||
:id-key :id))
|
||
(setq controller
|
||
(etaf-data-controller source :page-size 10 :auto-load t))
|
||
```
|
||
|
||
Controller 通过 `etaf-data-items`、`etaf-data-status`、`etaf-data-error`、`etaf-data-total`、`etaf-data-query`、`etaf-data-page`、`etaf-data-page-size` 和 `etaf-data-selection` 暴露响应式 ref。操作使用 `etaf-data-load`、`etaf-data-reload`、`etaf-data-mutate`、`etaf-data-set-query`、`etaf-data-next-page`、`etaf-data-previous-page`、`etaf-data-select`(追加式多选)、`etaf-data-select-one`(互斥单选)、`etaf-data-selected-item` 和 `etaf-data-stop`。当 Controller 的 `:auto-load` 为 nil 时,命令式的 next/previous 也会自动 reload;`:auto-load t` 时仍由 reactive effect 负责 reload,避免重复请求。
|
||
|
||
`etaf-data-selected-ref` 会为某个 row identity 返回稳定的 boolean ref;只有该
|
||
identity 进入或离开主 selection 时它才会变化,应用直接写
|
||
`etaf-data-selection` 时也一样。DataGrid 默认把这些 ref 与 keyed retained row
|
||
owner 配合使用,因此单选变化只会使旧行和新行失效,而不是整个可见页。
|
||
selection 由 Controller 外部拥有时,仍可使用自定义 `:row-selected-p`。稳定 identity
|
||
继续来自必选的 `:row-key`;DataGrid 没有第二个 selection-key prop。
|
||
|
||
`etaf-data-controller` 支持用于稳定选中行查找的 `:item-key`。在 Component
|
||
setup 中创建时,它的内部 effect Scope 会自动归当前 Component Scope 所有;如果
|
||
需要接入其他显式 owner,可以传入 `:owner-scope`。`etaf-data-mutate` 会在 reload
|
||
成功后返回 source 的变更结果,因此存储适配器可以直接暴露插入 id 或 changes,
|
||
不需要应用层再维护一条专用通道。
|
||
|
||
官方 DataGrid 是普通 Component:
|
||
|
||
```elisp
|
||
(require 'etaf-ui)
|
||
|
||
(etaf-mount
|
||
"*etaf-grid*"
|
||
(etaf-view
|
||
(etaf-data-grid
|
||
:controller controller
|
||
:columns '((:key :id :label "ID")
|
||
(:key :name :label "Name"))
|
||
:row-key (lambda (row) (plist-get row :id)))))
|
||
|
||
(etaf-data-mutate controller 'insert '(:id 3 :name "Alan"))
|
||
```
|
||
|
||
详情视图应使用 `(etaf-data-selected-item controller)`,不要在每个页面重复写
|
||
identity 匹配。source 可以返回类似 `(:operation insert :id 3 :changes 1
|
||
:value ...)` 的规范化变更 plist;ETAF 会在刷新 controller 的同时保留这个结果。
|
||
|
||
DataGrid 要求 `:row-key` 对每一行返回非 nil 的稳定标量。它通过普通 Host 和 slot 投影 loading、error、empty、header、rows 和 footer,不是第二种 data 或 Component 模型。
|
||
|
||
存储不绑定 SQLite。PostgreSQL、REST、文件或 ORM 集成都应该提供同样契约的具体 Data Source;它们是可选集成,不改变 ETAF 的用户模型。
|
||
|
||
独立的 `etaf-sqlite` 包是第一个具体存储 source:
|
||
|
||
```elisp
|
||
(require 'etaf-sqlite)
|
||
|
||
(let* ((table (etaf-sqlite-table
|
||
'tasks
|
||
(list (etaf-sqlite-column :id "id"
|
||
:type 'integer :primary t)
|
||
(etaf-sqlite-column :title "title" :type 'text))
|
||
:id))
|
||
(database (etaf-sqlite-database "tasks.sqlite" table))
|
||
(source (etaf-sqlite-source database)))
|
||
(etaf-sqlite-initialize database)
|
||
(let ((controller (etaf-data-controller source :auto-load t)))
|
||
(etaf-data-mutate controller 'insert
|
||
'(:id 1 :title "Write the guide."))
|
||
(message "%S" (etaf-value (etaf-data-items controller)))
|
||
(etaf-data-stop controller)))
|
||
```
|
||
|
||
这个包使用 Emacs 内置 SQLite 能力,有意不增加 ORM 层。其他存储包应直接实现 `etaf-data-source`。
|
||
|
||
## 11. Resource 与 raw Ebox
|
||
|
||
Resource 是 Scope 所有的同步 loader:
|
||
|
||
```elisp
|
||
(let ((resource
|
||
(etaf-resource
|
||
(lambda ()
|
||
(etaf-resource-result
|
||
"loaded"
|
||
:cleanup (lambda () (message "resource released")))))))
|
||
(message "%s: %s"
|
||
(etaf-resource-status resource)
|
||
(etaf-resource-value resource))
|
||
(etaf-resource-dispose resource))
|
||
```
|
||
|
||
Loader 错误会保存在 `etaf-resource-error`;cleanup/type 错误保持可见。`etaf-error-boundary-run` 只处理它的 function body 抛出的错误:
|
||
|
||
```elisp
|
||
(etaf-error-boundary-run
|
||
(lambda ()
|
||
(let ((filename "README.md"))
|
||
(with-temp-buffer
|
||
(insert-file-contents filename)
|
||
(buffer-string))))
|
||
(lambda (condition)
|
||
(message "Read failed: %S" condition)
|
||
nil))
|
||
```
|
||
|
||
公共 View 语法只接受 Host 和 Component,不接受裸 Ebox Node。需要更低层端口的框架集成直接使用 Ebox typed TextNode 和 BoxNode constructor;应用 View 始终走普通 Host/Component lowering 路径。
|
||
|
||
## 12. Playgrounds
|
||
|
||
如果需要小型、可复制的 core 模式,可以加载 `examples/` 中的可执行应用:
|
||
|
||
```elisp
|
||
(add-to-list 'load-path "/path/to/github/etaf/examples")
|
||
(require 'etaf-counter-example)
|
||
(etaf-counter-example-open)
|
||
```
|
||
|
||
计数器示例演示保留式状态、computed 值、Event、Action 和可聚焦 Host。`etaf-data-example-open` 演示 mounted 后的 Data 加载、查询、选择、变更和显式 controller 释放。`etaf-resource-example-open` 演示延迟 Resource 加载、可见错误状态、reload cleanup 与 Scope 释放。每个示例要教授的所有权规则见 [`examples/README.zh-CN.md`](../examples/README.zh-CN.md)。
|
||
|
||
独立的 ETAF Playground 是一个完整的 ETAF 应用示例:
|
||
|
||
```elisp
|
||
(require 'etaf-playground)
|
||
(etaf-playground-open)
|
||
```
|
||
|
||
需要展示官方目录时:
|
||
|
||
```elisp
|
||
(etaf-playground-open-ui)
|
||
```
|
||
|
||
`etaf-playground-close` 会卸载并删除默认 Playground buffer。它与 `ebox-playground` 独立,核心 ETAF 不会自动加载任一 Playground。
|
||
|
||
独立的 Ebox Playground 用来展示底层布局契约:
|
||
|
||
```elisp
|
||
(require 'ebox-playground)
|
||
(ebox-playground-open)
|
||
```
|
||
|
||
它只依赖 Ebox。检查 Ebox Box 和 Grid 布局时使用它;检查 Component、Runtime、Data 和官方目录时使用 `etaf-playground`。
|
||
|
||
## 13. 性能记录
|
||
|
||
对任意应用负载启用通用记录器,然后打开普通的 `tabulated-list-mode` 面板:
|
||
|
||
```elisp
|
||
(require 'etaf-performance)
|
||
;; 在已挂载 ETAF Runtime 的 buffer 中:
|
||
(etaf-performance-mode 1)
|
||
(etaf-performance-show)
|
||
```
|
||
|
||
正式复现前执行 `M-x etaf-performance-clear`。复现后在面板按 `c`,或者执行
|
||
`M-x etaf-performance-copy-report`,可把环境、汇总、operation、GC 和阶段数据
|
||
完整复制到剪贴板;按 `w` 或执行 `M-x etaf-performance-export` 可导出 `.eld`
|
||
文件。报告和面板 header 还会显示电源来源、低功耗模式、native JIT 状态与系统
|
||
负载。这两种报告都可以直接发送给分析者。
|
||
|
||
记录器只消费 Runtime 的公共 observer 报告,不安装 advice。Runtime 的 Event、
|
||
Action、mount、flush 和 unmount 会创建有界 operation 记录;同一 operation 内,
|
||
Ebox、TP、Data、Resource 与 SQLite 可以贡献按 sequence 排列的 flat provider
|
||
阶段。阶段可能重叠,因此不声称 exclusive/self 时间;error 和 quit 会先被记录,
|
||
再保持原条件继续抛出。
|
||
|
||
`etaf-performance-summary` 只在请求时计算分组后的 p50/p95/max;
|
||
`etaf-performance-operation-stage-summary` 会按 provider category 汇总某条记录
|
||
中的 flat 阶段。`etaf-performance-records` 返回 operation/stage 的防御性快照,
|
||
调用方修改返回值不会影响保留的历史。
|
||
该函数可选的数字 runtime ID 来自 observer 报告,可隔离 buffer 同名重建前后的
|
||
记录。summary/report 函数省略参数时读取全部历史,显式传入 `nil` 时保持空结果。
|
||
报告中的环境是导出时观测的状态,不是每次历史操作的状态。Runtime 耗时截至
|
||
同步调用返回,不能证明输入到画面呈现的延迟;逐次前后台、目标与回调 wall/CPU/GC
|
||
检查使用[GUI action 测量入口](../scripts/README.md#measuring-an-existing-gui-action)。
|
||
|
||
没有经过内置公共边界的应用操作可以使用
|
||
`etaf-performance-call-operation`/`etaf-performance-with-operation`;它们直接委托
|
||
同一个 Runtime operation 边界。采集结束后关闭 mode,只解除当前 Runtime 的
|
||
observer,不修改任何函数。
|
||
|
||
## 14. 公共 API 速查
|
||
|
||
| API 家族 | 主要入口 | 何时使用 |
|
||
| --- | --- | --- |
|
||
| View 与 Runtime | `etaf-view`、`etaf-render`、`etaf-mount`、`etaf-unmount`、`etaf-runtime-flush`、`etaf-runtime-snapshot` | 构建、渲染、挂载、flush 或显式导出已提交应用 |
|
||
| Component | `etaf-define-component`、`etaf-current-prop`、`etaf-current-slots`、`etaf-component-set-styles`、`etaf-component-redefine-run` | 复用 View、保留局部状态、设置 authoring 样式或显式重载代码 |
|
||
| 响应式状态 | `etaf-ref`、`etaf-value`、`etaf-set-value`、`etaf-computed` | 保存或派生状态 |
|
||
| 响应式 effect | `etaf-watch`、`etaf-watch-effect`、`etaf-effect-scope`、`etaf-scope-run` | 观察状态或同步外部资源 |
|
||
| 生命周期 | `etaf-on-mounted`、`etaf-on-updated`、`etaf-on-unmounted` | 绑定 Component 生命周期工作 |
|
||
| Context | `etaf-provide`、`etaf-inject`、`etaf-theme-provide`、`etaf-theme-token`、`etaf-theme-resolve-palette`、`etaf-theme-current-mode` | 跨层级共享依赖并解析语义 Theme palette |
|
||
| 事件与 focus | `etaf-dispatch-event`、`etaf-activate`、`etaf-focus`、`etaf-focus-next`、`etaf-focus-previous`、`etaf-input-mode` | 进入交互 Runtime |
|
||
| Action | `etaf-action-define`、`etaf-dispatch` | 命名和复用业务变更 |
|
||
| Behavior | `etaf-behavior-create`、`etaf-define-behavior`、`etaf-current-behavior-context`、`etaf-focusable`、`etaf-toggleable` | 复用非视觉交互 bundle |
|
||
| Data | `etaf-data-source`、`etaf-data-controller`、`etaf-data-memory-source`、`etaf-data-*` | 查询、分页、变更、选择、解析选中项和停止数据 |
|
||
| Resource | `etaf-resource`、`etaf-resource-result`、`etaf-error-boundary-run` | 管理 loader 状态和 cleanup |
|
||
| 官方 UI | `require 'etaf-ui`、`etaf-button`、`etaf-checkbox`、`etaf-number-input`、`etaf-label`、`etaf-panel`、`etaf-data-grid` | 使用现成 Component |
|
||
| Playground | `etaf-playground-open`、`etaf-playground-open-ui`、`etaf-playground-close`、`ebox-playground-open`、`ebox-playground-close` | 探索对应层 |
|
||
| 性能分析 | `etaf-performance-start`、`etaf-performance-stop`、`etaf-performance-mode`、`etaf-performance-show`、`etaf-performance-copy-report`、`etaf-performance-export`、`etaf-performance-environment-data`、`etaf-performance-records`、`etaf-performance-summary`、`etaf-performance-operation-stage-summary`、`etaf-performance-call-operation`、`etaf-performance-with-operation` | 通过公共 observer 定位通用 operation 的耗时并分享报告 |
|
||
|
||
大多数应用一开始只需要 `etaf-view`、`etaf-mount`、`etaf-define-component`、`etaf-ref` 和事件 callback。其余 API 都是可选能力,不是理解核心语法的前置条件。
|
||
|
||
## 15. 常见错误
|
||
|
||
- 所有属性必须放在第一个子节点之前。
|
||
- 使用 `:font-weight 'bold`,不要使用 `:font-weight :bold`;weight 是 Elisp symbol 值,不是属性 keyword。
|
||
- 结构性 View form 不要 quote。
|
||
- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 typed View 时,使用 `(expr FORM)`。
|
||
- 默认 outlet 使用 `(slot)` 或 `(slot FALLBACK...)`;命名内容使用 `:name 'header`。
|
||
- 不要在 render 中写状态;使用事件、Action、watch callback 或 Effect。
|
||
- 产品级控件使用 `etaf-ui` Component;core Host 只是结构基础。
|
||
- owner 不再需要时,停止 Data Controller 并卸载 Runtime。
|
||
|
||
普通应用在启用 lexical-binding 的 `.el` 文件中定义并挂载 Component。可选的
|
||
Playground 使用 inert `.etaf` 结构与显式注册的 `.el` companion;core 不会自动
|
||
发现或执行这组文件。同一 batch 内的响应式写入合并为一次 generation publication;
|
||
发布失败可以重试,non-converging effect 会报告错误,不会持续占用事件循环。
|