Files
wasmeld/crates/wasmeld-console/web
Maofeng 9ff3223c44 refactor(console-web): centralize page lifecycle and UI composition
Declare navigation, page metadata, lazy loaders, and keep-alive policy in one registry. Retain primary iframe pages until a continuous inactive TTL expires and expose typed show, hide, and unload events through the Page SDK.

Move business-aware components out of the UI primitive directory, migrate views and Host dialogs to variant primitives and local Tailwind classes, and remove the global component style layer. Keep native dialogs inside the overlay flex context so they remain centered.

Cover iframe expiration, lifecycle dispatch, UI dependency direction, and global style ownership with focused tests.
2026-07-30 15:16:02 +08:00
..

Wasmeld Console Web

管理面使用 Solid 和 Tailwind CSS v4。它不是传统单文档 SPA,而是由一个常驻 Host 文档和一个可替换 Page iframe 组成:

index.html / src/host
├── 导航、搜索、弹窗、通知
├── 管理 API 轮询和共享状态
└── page.html?view=<view>&instance=<n>
    └── src/pages/<view>

普通页面切换时,Host 先发送 dispose,再销毁 iframe 并创建新的文档。页面所属的 Solid reactive owner、事件监听器、第三方 UI 库和页面局部缓存会随文档一起释放,避免 长期导航后把所有页面资源都留在同一个 JavaScript realm 中。

需要保留表单、筛选器或昂贵页面状态时,可以在 src/host/app.tsxNAVIGATION 配置中设置 keepAlive: true。当前服务版本和运行设置启用保活。Host 会隐藏而不是销毁 这些 iframe,并保持其 DOM、JavaScript realm 和 Solid 状态:

  • 离开时发送 deactivate,页面应暂停轮询、媒体、动画或其它后台工作。
  • 返回时发送 activate,恢复页面任务,iframe 的 instancetimeOrigin 不变。
  • LRU 淘汰、关闭 Host 或普通页面离开时发送 dispose,随后销毁文档。
  • MAX_KEEP_ALIVE_IFRAMES 限制保活文档数量;活动的非保活页面最多临时多占一个 iframe。

页面可以使用 PageProps.active 响应生命周期,也可以监听 wasmeld:activatewasmeld:deactivatewasmeld:dispose 或统一的 wasmeld:lifecycle 事件。页面自身不应通过 display: none 判断状态,因为 Host 可能改变具体的隐藏实现。

export default function StreamPage(props: PageProps) {
  createEffect(() => {
    if (props.active()) {
      resumeStream();
    } else {
      pauseStream();
    }
  });
  onCleanup(closeStream);
  // ...
}

iframe 是资源生命周期边界,不是安全边界。Host 和 Page 都是 Wasmeld 自己构建并同源 发布的可信代码;Wasm 服务的安全边界仍然在后端 Wasmtime Sandbox 和 Host Capability Registry 中。

目录

src/host/          常驻 Host Shell、iframe LRU 和 Host-owned dialogs
src/components/ui/ 跨页面复用的无业务 UI
src/lib/           API client、领域模型和纯函数
src/primitives/    Solid reactive primitivesSolid 不使用 React Hooks 约定
src/pages/         每个 iframe 页面一个目录和独立懒加载 chunk
src/sdk/           Host/Page postMessage 协议与两侧 client
src/styles/        Tailwind v4 theme 和共享组件样式

src/sdk/protocol.ts 中的 SDK_VERSION 是文档间协议版本。新增可选消息可以保持原版本; 删除字段、改变字段语义或产生不兼容状态时必须升级版本,并让两侧同时发布。消息接收端 同时检查 origin、source、channel 和 version,不接收任意窗口的控制命令。

Host 会继续向隐藏的保活页面发送只读状态快照,但拒绝其管理命令。这样页面可以在恢复时 立即显示最新 Runtime 状态,同时停用后的定时器不能意外触发注册、启停或 Deployment 操作。

浏览器 Tab 状态

同源普通 Tab 和已安装 PWA 窗口通过 BroadcastChannel 共享 Host 全局状态。状态按 所有权分为三层:

状态 跨 Tab Host 到 Page 生命周期
Runtime 快照、连接、API 地址、控制操作状态 始终共享 分发给已存在的 iframe Host Tab
当前路由、搜索、弹窗、toast、iframe LRU 不共享 当前 Tab 自己管理 Browser Tab
页面筛选、表单、滚动位置、页面资源 不共享 不进入 Host iframe document

src/host/tab-sync.ts 使用独立版本的 Host Tab 协议:

  • 每个 Host Tab 有随机 sender 和单调递增 sequence
  • 新 Tab 发送 hello,已打开 Tab 立即返回当前 Host 全局状态。
  • 所有 Host Tab 都可以发布,不依赖可能失效的 leader。
  • 接收方拒绝自身消息、旧序列和字段不合法的消息。
  • 应用远端状态时抑制本地广播 effect,避免 Tab 间回声循环。
  • 浏览器不支持 BroadcastChannel 时退化为单 Tab,不影响页面和 API 操作。

Host 收到本地或远端全局状态后,只遍历当前 Tab 的 FrameCache。活动 iframe 和隐藏的 keep-alive iframe 会收到最新快照;从未打开或已被销毁的 Page 没有同步目标,创建并 发送 ready 后才取得当时的最新状态。Page 私有信号永远不会上传到 Host Tab channel。

开发

先在仓库根目录启动 Rust 后端:

cargo +stable run -p wasmeld-console

再启动 Web 开发服务器:

cd crates/wasmeld-console/web
npm ci
npm run dev

打开 http://127.0.0.1:3000。Vite 开发环境默认连接 http://127.0.0.1:8080;设置页可以覆盖该地址。Debug Cargo 构建不会调用 npm 因此 Rust 与 Web 的增量开发互不阻塞。

常用检查:

npm run format:check
npm run lint
npm test
npm audit --audit-level=high

发布与嵌入

Release 构建由 ../build.rs 自动执行:

cargo +stable build --release -p wasmeld-console

构建脚本使用 npm ci 严格读取 package-lock.json,将 Vite 输出写入 Cargo OUT_DIR,再由 Rust include_dir! 嵌入可执行文件。发布机器必须提供 Node.js 22.13+ 和 npm。可以用 NPM=/absolute/path/to/npm 指定 npm;只有需要调试嵌入行为时,才在 非 Release 构建中设置 WASMELD_BUILD_WEB=1

Release 管理 Listener 同时服务 //page.html、静态资源、PWA manifest 和 /sw.js。资源路径必须精确存在,不做 SPA fallback。带内容哈希的 assets/ 使用长期 immutable 缓存,其它入口使用 no-cache

PWA 边界

一个 Host-owned service worker 管理整个应用:

  • 构建时预缓存 Host、Page 和带哈希静态资源。
  • 文档请求使用 network-first,离线时分别回退到 index.htmlpage.html
  • /api/*/healthz 永远走网络,不缓存运行状态或管理操作。
  • 新 worker 激活时删除旧的 wasmeld-* 缓存。

PWA 只提供安装和静态壳离线能力。没有后端连接时页面会显示离线状态,不会用旧 API 响应伪造 Runtime 状态。