Skip to content

Latest commit

 

History

History
271 lines (210 loc) · 19.1 KB

File metadata and controls

271 lines (210 loc) · 19.1 KB

explore-react Developer Guide

这份文档不是零碎备忘录,而是重新上手这个仓库时的开发引导。目标是:先跑起来,再改 Koka 代码,再验证浏览器和测试链路,最后再扩展新的 demo。

先从哪里开始

第一次回到仓库时,优先按这个顺序走:

  1. 先看 package.json 里的脚本,确认日常入口还是 yarn devyarn buildyarn test:koka;首次体验先运行 yarn example:first-component
  2. 组件作者先看 docs/quick-start.mddocs/component-authoring.md;不要从 runtime snapshot 实现反推日常 API。
  3. 再看 app.kk,确认当前浏览器桥只暴露哪些 Koka 入口。
  4. 然后看 explore/react/*demo/* 的边界:前者是库,后者是 demo 和业务。
  5. 开始改代码前,先跑一次 yarn build,确认自己不是站在坏状态上继续开发。

仓库结构

  • 仓库根目录:就是 Koka 源码根目录,编译时直接把 repo root 当成模块搜索根。
  • app.kk:浏览器入口,只暴露 boot、事件桥接和 runtime snapshot 导入导出。
  • explore/react.kk:普通组件唯一的 author import,只 re-export core/action/state,不包含 runtime/inspection/renderer。
  • explore/react/core.kkaction.kkstate.kk:component authoring 实现模块;advanced host/tests 可以按边界显式导入。
  • explore/react/runtime.kk:browser/app host 使用的 registered callback、scheduled effect 与 snapshot transport。
  • explore/react/inspection.kk:tests/devtools 使用的 VDOM、event registry 与 runtime entry 只读查询。
  • explore/react/renderer.kk:host/tests 使用的 render、diff 与 patch;不要导入业务 view。
  • demo/*:具体 demo、布局、组件、路由和测试辅助。
  • examples/first_component/*:可直接执行的 component authoring 参考;component.kk 只使用公共入口,main.kk 是明确的 advanced host。
  • demo/runtimeframe.kk:app 边界的 runtime owner,同时持有业务 model 和框架 state tree。
  • runtime/*:只放 DOM 和系统边界的 FFI,不要把业务逻辑塞进来。
  • scripts/build-koka.mjs:从仓库根目录调用 Koka,输出到 src/generated/koka
  • scripts/test-koka.mjs:从仓库根目录执行 tests_main.kk
  • src/main.js:Vite 主机入口,只负责挂接生成后的 Koka 导出。

日常开发命令

最常用的是下面这组:

yarn dev
yarn build
yarn check
yarn test:koka
yarn example:first-component

分别表示:

  • yarn dev:先编译 Koka,再启动 Vite。做 UI、交互、浏览器联调时优先用它。
  • yarn build:koka:只重编译 Koka,适合只改 .kk 文件时快速验证生成产物。
  • yarn build:Koka + Vite 全链路构建,改模块边界、入口导出、浏览器桥时先跑它。
  • yarn check:Koka 构建 + Vite bundling 检查,适合看宿主层是不是也被带坏了。
  • yarn test:koka:跑 Koka 侧快速测试,不依赖浏览器。
  • yarn example:first-component:编译并运行 first-component,打印初始 render、typed action 和 transition 后 render;同时校验该输出。

模块缓存或生成产物异常时,优先用这个最便宜的重置:

rm -rf .koka-build .koka-test && yarn build

如果需要直接绕过脚本看底层 Koka 命令,使用:

koka --target=jsnode --builddir=.koka-test --execute tests_main.kk
koka --target=jsweb --library --builddir=.koka-build --outputdir=./src/generated/koka --output=koka-app app.kk

Koka 编译规则

这个仓库最容易出错的是模块搜索根。

  • 编译 Koka 时,cwd 应该直接对齐到仓库根目录,否则像 demo/model 这样的导入可能找不到。
  • 导入路径以仓库根为根:import demo/model,不要写 import koka/demo/model
  • 生成入口文件名不稳定,所以构建脚本靠扫描 export function boot( 找入口,不要手写猜测产物名。
  • 浏览器桥应该保持很薄,目前只导出少量入口给宿主层,例如 boot_with_snapshot、点击/输入派发、路由派发和 snapshot 导出。

Yarn 与 Vite 工作流

  • 仓库使用 Yarn Berry,日常命令统一走 yarn,不要切回 npm。
  • Vite 默认端口配置在 vite.config.js;要固定本地地址时可用:
yarn dev --host 127.0.0.1 --port 4173
  • 如果端口被占用,Vite 会自动换端口,后续 chrome-devtools 要跟着实际端口走。
  • 页面起来了但交互失效时,先检查 src/generated/koka-entry.mjssrc/main.jsruntime/inline/dom.js 的桥接变量名是否一致。

Chrome DevTools 命令流程

这里要记的是实际命令,不是抽象描述。一个完整流程通常是:

yarn dev --host 127.0.0.1 --port 4173
chrome-devtools new_page http://127.0.0.1:4173/
chrome-devtools list_pages
chrome-devtools select_page <page_id>
chrome-devtools take_snapshot --filePath .tmp-devtools-snapshot.txt
chrome-devtools take_screenshot --fullPage --filePath .tmp-devtools-full.png

常用调试动作:

  • 新开页面:chrome-devtools new_page http://127.0.0.1:4173/
  • 列出现有页面:chrome-devtools list_pages
  • 选择当前页面:chrome-devtools select_page 7
  • 保存结构快照:chrome-devtools take_snapshot --filePath .tmp-devtools-snapshot.txt
  • 保存整页截图:chrome-devtools take_screenshot --fullPage --filePath .tmp-devtools-full.png
  • 如果已经拿到 snapshot 里的 uid,后续可继续用 clickfillpress_key 做交互回归。

浏览器检查时,优先关注:

  • 首屏文案和面板数量是否是新版本,不是旧缓存。
  • 事件委托属性是否还在:data-k-clickdata-k-inputdata-k-enter
  • 交互冒烟顺序是否还通:新增任务、回车提交、切换 done、切换 filter、删除、清空 completed。
  • 大屏布局是否真的吃满宽度,而不是把信息挤进一条窄侧栏。

测试策略

默认先跑 Koka 测试,再跑浏览器。

  • 纯逻辑、reducer、render、effect handler 优先放进 yarn test:koka
  • HTML 输出测试直接比较 render_node(...) 的字符串,适合做轻量 snapshot。
  • confirm_action 这类效果,使用 handler 模拟接受/拒绝,不要依赖浏览器原生弹窗。
  • wait_ms 这类 timer 效果,用 handler 记录延迟值,做“立即完成”的快速测试。
  • 浏览器测试只验证桥接、真实 DOM patch 和端到端交互链路。

新增 Demo 的推荐步骤

现在仓库已经开始有顶层 route,后续加新 demo 建议按这个顺序:

  1. demo/ 新建一个单独模块,例如 mydemo.kk
  2. 暴露一个顶层面板函数,例如 my_demo_panel(model) : vnode
  3. demo/model.kk 里补 route 名称归一化和 route action。
  4. 在 route bar 里加一个新 tab。
  5. demo/layout.kk 里把新 route 接进页面分发。
  6. 如果 demo 有纯逻辑,就把测试加到 demo/tests.kk 或独立测试模块。

GitHub issue 与 PR 约定

  • issue 和 PR 的标题、正文都使用中英双语;标题统一写成 中文 / English,方便两种语言的读者搜索和识别。
  • 正文固定分成两个完整章节:先写 # 中文,再写 # English。不要逐行穿插翻译,也不要只在一个章节里补充另一种语言没有的信息。
  • issue 的两个章节都应独立包含用户问题、目标、范围、验收标准和非目标;PR 的两个章节都应独立包含用户可见结果、安全/生命周期、文档、验证和关联 issue。
  • 会影响跟踪结论的进度评论、review 回复和方案变更,也使用相同的 # 中文 / # English 分段;简短机械通知可以不重复。
  • 代码标识符、命令、issue/PR 编号和链接保留原始写法,避免翻译后难以搜索。

代码风格约定

  • 不要写单行 accessor 包装函数,例如:
    pub fun task_title(item : task) : string
      task/title(item)
    这类函数只是给 struct 字段起别名,拆得越多越难找逻辑。调用方直接内联 task/title(item)item.title,保持代码密度。
  • 只在以下情况才抽函数:有额外逻辑(条件、组合、副作用),或者跨模块需要稳定的公开接口名称。

组件本地状态约定(typed store + actions)

组件交互状态以 typed store + serializable actions 为默认方案。它保留 React reducer 的简单心智模型,同时满足 Koka 严格类型、HMR 和 snapshot 恢复需求。

  • 普通业务 view 统一 import explore/react;不得导入 explore/react/runtimeexplore/react/inspectionexplore/react/renderercore/action/state 保持实现分层,但不再要求组件作者理解三条 import。

  • 业务组件用 (state, dispatch) = use_store(spec, initial = ...) 读取 reducer pair,不额外暴露 binding record。

  • UI 事件优先用 on_store_click(name, action = ..., dispatch = ...) / on_store_input(...) 发送 typed action,不直接操作 state tree。

  • domain action 事件优先用 on_action_click(name, action = ..., dispatch = ...) / on_action_input(...) / on_action_enter(...),不要在每个 element 内重复 forwarding closure。

  • 同一事件固定按顺序发送一个完整 domain action、再按条件发送一个 component-store action 时,使用 action_store_transition(...) 构造 handler,并交给现有 on_local_click(...) / on_local_enter(...);不要为每种 DOM event 增加 transition helper。

  • action_store_transition(...) 总是先发送 domain action;store_when 只控制后续 component action。包含多步更新、复杂分支或直接 model 输入时才手写 on_local_* handler。

  • 一个 store 把 state 类型、action 类型、纯 reducer、action codec 和显式 recovery policy 定义在一起;codec 只在 store 定义处出现,不传进组件调用。

  • store 统一通过 labelled snapshot_store(...) / replay_store(...) 构造,不让业务模块直接依赖 Store_spec(...) 的内部字段顺序。State_codec(...)Action_codec(...) 仍显式写出 schema/version/decode/encode

  • snapshot_store(...) 适合累积、toggle 或无法安全压缩的状态;replay_store(...) 只适合有明确 session 边界的纯 component actions,并使用 Replay_start / Replay_replace(slot) / Replay_reset 声明恢复语义。

  • replay slot 必须是有限、稳定的语义名称;不要用动态业务 id/用户输入制造 slot,也不要通过丢弃最早 action 强行限制日志。无法安全压缩时回到 snapshot store。

  • scope/path/slot/tree 属于 framework/runtime 细节;业务 view 只声明 keyed boundary,不手工拼 path。

  • 单个 keyed boundary 优先写成 component(group, key) { ... } / feature_root(group, key) { ... },用 Koka trailing-lambda 语法让 lifecycle 边界有鲜明特征;列表仍用 labelled components(...) 保持 key/render 映射清楚。

  • component(...) / components(...) 表示 ordinary child:当前 feature 仍 render、但 child 不再出现时,其 local store 与 effect metadata 会自动清理。filter/条件分支隐藏 child 等同 unmount。

  • feature_root(...) 表示 persistent boundary:整个 feature 未 render 时保留 snapshot,供 route 返回、HMR 和 reload 恢复。不要只为保留一个 input draft 就滥用 feature root。

  • feature component 自己持有稳定的 feature_root(...),签名返回 app_view vnode,并用 labelled key 隔离 local store、effect、listener 与 DOM marker;domain props 是否共享由调用方决定。不要暴露 runtime 四元组或新增 run_*_panel adapter。

  • 只有真正支持多实例隔离的 feature 才公开 key;app-owned singleton(例如全局 Dialog overlay)使用固定 identity,不提供只隔离一部分 runtime surface 的伪多实例参数。

  • feature_root(...) 会安装 opaque feature identity;内部 helper 使用 feature_key() / feature_marker(name) 派生 VDOM sibling key 与 DOM effect marker,不层层转发 panel_key,也不读取 raw group/key。

  • feature_node_key(...) / feature_dom_marker(...) 只保留给 framework/runtime inspection 与 tests,不进入业务 view。

  • 整棵 app tree 只在集成层调用一次 run_component(owner, group = ..., key = ..., render = fn(owner) ...),不要让 layout 手工合并 effects/registries。

  • 高层 view component 保持一个主要 domain value 位置参数,其余 callback/config props 使用 labelled arguments,例如 on_input = ...on_submit = ...on_select = ...

  • domain reducer/workflow 不读取或写入 child component store;需要 draft 等当前值时,把值放进 serializable domain action,再由组件用 action_store_transition(...) 协调一个简单 store transition,或在复杂情况下使用 on_local_*

  • action 必须可序列化,方便事件日志、恢复、回放,以及后续 agents/actions/store 工具链。

推荐形态:

val (Task_editor_state(editing, draft), dispatch_editor) = use_store(
  task_editor_store,
  initial = Task_editor_state(False, item.title))

input_text(
  draft,
  input = on_store_input(
    "draft-input",
    action = Change_draft,
    dispatch = dispatch_editor))

val save_edit = action_store_transition(
  Save_task(draft),
  dispatch = dispatch,
  store_action = Finish_edit,
  store = dispatch_editor,
  store_when = draft != "")

button(
  "Save",
  click = on_local_click("save-edit", save_edit))

完整选择规则和 action observation 顺序见 docs/action-store-transitions.md

state(...) / state_pair(...) 可以用于没有业务 action 语义的简单实验,但新业务组件只要状态由用户事件更新,就优先定义 typed store。不要继续扩散显式 codec、hook index 或手工 path 的调用形式。

Action observation 约定

  • domain action 与 component store action 统一编码为 action_envelope,字段为 sourcetargetschemaversionpayload
  • typed dispatch 必须在 reducer/workflow 前调用 emit_action(...);调用时使用 labelled arguments,让 source/target/codec/action 的含义清楚可见。
  • component store 由 use_store(...) 返回的 dispatch 自动发 observation,业务 view 不重复埋点。
  • app/runtime 边界通过 run_runtime_action_observed(...) 获取有序 action 列表;不需要观察的测试或内部调用使用 run_runtime_action(...)
  • observation 表示“已发送 intent”,不表示 reducer 成功或外部 effect 已提交。confirm 拒绝的 action 仍可被观察。
  • replay_store(...) 的 scoped pure component-action log 可以进入 component snapshot,但与统一 observation stream 分开;未建立权限、effect response 和幂等策略前,不自动 replay domain/external-effect actions。
  • 新增 domain action 时,codec 与 action/reducer 放在同一 feature 模块,并覆盖 schema/version/payload 的 round-trip 测试。

Element 调用约定

  • element 的主要内容保持第一个位置参数:容器传 list<vnode>,文本元素传 string
  • 常用 DOM 属性和事件压平为 labelled arguments,例如 class = ...key = ...click = ...input = ...
  • 不使用 attrs = {...}, children = ... 这种嵌套记录写法。
  • 少见 DOM 属性和事件才放进 extra_attrs / extra_events escape hatch。
div([
  strong(item.title, class = "task-title"),
  button("Edit", class = "button", click = edit_listener),
], key = item.id.show, class = "task-item")

Runtime snapshot 与 HMR

  • component runtime tree 不放回业务 model;app 边界通过 runtime_frame 持有。
  • feature/view API 不得接收或返回 list<state_entry>;render transition 统一通过 run_runtime_render(...)
  • parent component 不读取 child local store 做业务汇总;确实需要上层观察的数据提升到 domain model,纯调试统计放到 framework inspector。
  • 每次 app render 只安装一个 stateful component runtime;feature 通过 scoped vnode component 在同一 handler 内组合。
  • scheduled effects 按组件求值顺序收集;不要把跨组件的 effect 顺序当作数据依赖。
  • 不需要释放资源的 render 后工作使用 state_effect("name", deps) { ... };subscription、observer、timer handle 等使用 labelled state_resource(name = ..., deps = ..., cleanup = ..., action = ...)
  • resource 在 deps 变化时先 cleanup 再 setup,并在 ordinary child unmount、reset_feature(...) 与 HMR dispose 时 cleanup;persistent feature 仅暂时隐藏时继续保留 resource。
  • cleanup closure 只存在于 host memory registry,不进入 snapshot;cleanup error 记录 host 日志且不得阻断后续 setup/boot。
  • snapshot entry 必须保留稳定 path、schema、version、payload;decoder 对 malformed payload、schema/version 不匹配安全回退。
  • 完整 snapshot 使用 respo/runtime-snapshot|1 顶层 header;decoder 兼容旧无 header 格式,unknown 顶层版本回退空树,单个 malformed entry 不影响其他合法 entry。
  • respo/component-scope 是 runtime-owned lifecycle marker,会进入 snapshot;业务模块不得读取、构造或修改。ordinary child sweep 由 framework visitation 驱动,不在 reducer 中重建 path。
  • replay entry 使用 respo/replay:<action-schema>,只由 replay_store(...) 读写;业务 view 仍只使用 (state, dispatch) = use_store(...)。恢复策略与选择标准见 docs/store-recovery.md
  • src/main.js 负责 localStorage 与 Vite HMR hand-off。修改浏览器桥时要验证 replacement 前 flush、dispose 和 pagehide 三条路径。
  • snapshot 只是组件临时状态恢复机制,不替代业务数据持久化。
  • lifecycle 规则和旧 snapshot 兼容限制见 docs/component-lifecycle.md

Koka 常见易错点

  • 跨模块使用的 structeffect、公开函数要显式 pub,否则拆文件后很容易编到一半才报不可见。
  • --execute 的测试脚本如果不在正确目录执行,模块解析会失败;优先把 cwd 对齐到仓库根目录。
  • effect handler 很容易意外带出额外效果,例如在 handler 子句里做递归处理,把 <div><local> 混进签名;先写最小 handler,再做组合。
  • snake_case 类型名的构造器不总是直觉,例如 test_result 对应 Test_result;找不到构造器时先怀疑这一层。
  • 分支里的顺序语句尽量保持简单;如果解析器报意外语法,先把字符串拼装或中间计算抽成独立函数。
  • FFI 只做 DOM 或系统边界;组件描述、状态、diff/patch 仍然保持在 Koka 内部。

遇到问题先看这里

  • 构建失败:先检查是不是模块搜索根不对,再检查 pub 可见性和生成入口。
  • 页面空白:检查 src/generated/koka-entry.mjs 是否重生成,以及 Vite 是否加载了新模块。
  • 点击没反应:检查 runtime/inline/dom.js 的 delegated event 是否还在调用正确的全局桥。
  • 地址栏 hash 和页面不一致:先看 route 是否在 commit 后同步回浏览器。
  • 输入值不同步:检查 sync_input 是否在 commit 后执行。
  • UI 显得花哨或浪费空间:优先调整信息密度、列宽和面板组织,而不是先把配色全部抹平。