这份文档不是零碎备忘录,而是重新上手这个仓库时的开发引导。目标是:先跑起来,再改 Koka 代码,再验证浏览器和测试链路,最后再扩展新的 demo。
第一次回到仓库时,优先按这个顺序走:
- 先看
package.json里的脚本,确认日常入口还是yarn dev、yarn build、yarn test:koka;首次体验先运行yarn example:first-component。 - 组件作者先看
docs/quick-start.md与docs/component-authoring.md;不要从 runtime snapshot 实现反推日常 API。 - 再看
app.kk,确认当前浏览器桥只暴露哪些 Koka 入口。 - 然后看
explore/react/*和demo/*的边界:前者是库,后者是 demo 和业务。 - 开始改代码前,先跑一次
yarn build,确认自己不是站在坏状态上继续开发。
- 仓库根目录:就是 Koka 源码根目录,编译时直接把 repo root 当成模块搜索根。
app.kk:浏览器入口,只暴露 boot、事件桥接和 runtime snapshot 导入导出。explore/react.kk:普通组件唯一的 author import,只 re-exportcore/action/state,不包含 runtime/inspection/renderer。explore/react/core.kk、action.kk、state.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 时,
cwd应该直接对齐到仓库根目录,否则像demo/model这样的导入可能找不到。 - 导入路径以仓库根为根:
import demo/model,不要写import koka/demo/model。 - 生成入口文件名不稳定,所以构建脚本靠扫描
export function boot(找入口,不要手写猜测产物名。 - 浏览器桥应该保持很薄,目前只导出少量入口给宿主层,例如
boot_with_snapshot、点击/输入派发、路由派发和 snapshot 导出。
- 仓库使用 Yarn Berry,日常命令统一走
yarn,不要切回 npm。 - Vite 默认端口配置在
vite.config.js;要固定本地地址时可用:
yarn dev --host 127.0.0.1 --port 4173- 如果端口被占用,Vite 会自动换端口,后续
chrome-devtools要跟着实际端口走。 - 页面起来了但交互失效时,先检查
src/generated/koka-entry.mjs与src/main.js、runtime/inline/dom.js的桥接变量名是否一致。
这里要记的是实际命令,不是抽象描述。一个完整流程通常是:
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,后续可继续用click、fill、press_key做交互回归。
浏览器检查时,优先关注:
- 首屏文案和面板数量是否是新版本,不是旧缓存。
- 事件委托属性是否还在:
data-k-click、data-k-input、data-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 和端到端交互链路。
现在仓库已经开始有顶层 route,后续加新 demo 建议按这个顺序:
- 在
demo/新建一个单独模块,例如mydemo.kk。 - 暴露一个顶层面板函数,例如
my_demo_panel(model) : vnode。 - 在
demo/model.kk里补 route 名称归一化和 route action。 - 在 route bar 里加一个新 tab。
- 在
demo/layout.kk里把新 route 接进页面分发。 - 如果 demo 有纯逻辑,就把测试加到
demo/tests.kk或独立测试模块。
- issue 和 PR 的标题、正文都使用中英双语;标题统一写成
中文 / English,方便两种语言的读者搜索和识别。 - 正文固定分成两个完整章节:先写
# 中文,再写# English。不要逐行穿插翻译,也不要只在一个章节里补充另一种语言没有的信息。 - issue 的两个章节都应独立包含用户问题、目标、范围、验收标准和非目标;PR 的两个章节都应独立包含用户可见结果、安全/生命周期、文档、验证和关联 issue。
- 会影响跟踪结论的进度评论、review 回复和方案变更,也使用相同的
# 中文/# English分段;简短机械通知可以不重复。 - 代码标识符、命令、issue/PR 编号和链接保留原始写法,避免翻译后难以搜索。
- 不要写单行 accessor 包装函数,例如:
这类函数只是给 struct 字段起别名,拆得越多越难找逻辑。调用方直接内联
pub fun task_title(item : task) : string task/title(item)
task/title(item)或item.title,保持代码密度。 - 只在以下情况才抽函数:有额外逻辑(条件、组合、副作用),或者跨模块需要稳定的公开接口名称。
组件交互状态以 typed store + serializable actions 为默认方案。它保留 React reducer 的简单心智模型,同时满足 Koka 严格类型、HMR 和 snapshot 恢复需求。
-
普通业务 view 统一
import explore/react;不得导入explore/react/runtime、explore/react/inspection或explore/react/renderer。core/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 边界有鲜明特征;列表仍用 labelledcomponents(...)保持 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,并用 labelledkey隔离 local store、effect、listener 与 DOM marker;domain props 是否共享由调用方决定。不要暴露 runtime 四元组或新增run_*_paneladapter。 -
只有真正支持多实例隔离的 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 的调用形式。
- domain action 与 component store action 统一编码为
action_envelope,字段为source、target、schema、version、payload。 - 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 的主要内容保持第一个位置参数:容器传
list<vnode>,文本元素传string。 - 常用 DOM 属性和事件压平为 labelled arguments,例如
class = ...、key = ...、click = ...、input = ...。 - 不使用
attrs = {...}, children = ...这种嵌套记录写法。 - 少见 DOM 属性和事件才放进
extra_attrs/extra_eventsescape hatch。
div([
strong(item.title, class = "task-title"),
button("Edit", class = "button", click = edit_listener),
], key = item.id.show, class = "task-item")- 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 等使用 labelledstate_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。
- 跨模块使用的
struct、effect、公开函数要显式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 显得花哨或浪费空间:优先调整信息密度、列宽和面板组织,而不是先把配色全部抹平。