Files
DCU_DesktopController/docs/compose/spec/tab-keepalive.md
T

94 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
feature: tab-keepalive
status: delivered
updated: 2026-09-16
branch: feat/tab-keepalive
commits: 9bb804ec350534e462599d9c2524eacc0a88d207..eaafee131211190928e0c035f7c0fd89ed722fd4
---
# Tab Keep-Alive(多 iframe 常驻)
## Report
**What was built** — `tabs.html` 从「单 iframe 换 src」改为「一标签一懒创建 iframe」:
切换只改 `visibility`/`pointer-events`,不改 `src`、不 `remove`。功能页文档常驻,
WebSocket、轮询、传输状态在切走再切回后仍在。F5/⟳(`esp32_server.py` 注入脚本)只
reload 激活的 `.page-frame`,不再重载外壳(重载外壳会销毁全部保活帧)。外壳判定改为
`#viewHost`。
**Verification** —
- `python -m py_compile esp32_server.py` → PASS
- `tabs.html` 内联脚本 UTF-8 抽出后 `node --check` → PASS
- 独立评审(9bb804e..eaafee1):规格符合 / 正确性 / 代码库一致性 均 PASS,无 critical
**Journey log** —
- 单 iframe `view.src=path` 是连接全灭的根因,不是各页轮询写错
- 保活隐藏用 `visibility` 而非 `display:none`,降低引擎对不可见文档的节流
- 注入 F5 必须认 `.page-frame.active`;外壳 `location.reload()` 会一次清掉所有保活文档
- 页内跳到「另一个已登记标签」时不再自动抢高亮(多帧语义下更安全;旧单 iframe 会)
- 懒创建时用 `activePage` 拦 load 抢高亮,避免 `dcu_tab` 被慢帧覆盖
## [S1] Problem
`tabs.html` 用**单 iframe** 承载功能页,切换标签时执行 `view.src = path`。
这会销毁上一页的整个文档:
- WebSocket(`app-lease`、调试通道)断开
- `setInterval` / `setTimeout` 轮询停止
- 电机页 EtherCAT/WiFi 传输状态、`transport-ecat` 的 `started` 标志丢失
- OTA 页 `ecat_load` 轮询、SPI 状态轮询冷启动
- 用户「调完一切、切走再切回」后连接全灭,表现为界面假死/全 `--`
外壳自身的 `app-lease.js` 仍在,但功能页内的连接与状态无法幸存。
这是结构性缺陷,不是某个页面的 bug。
## [S2] Design
### 外壳:一标签一 iframe,只 show/hide
- `TABS` 每项对应一个懒创建 iframe(`class="page-frame"`,`data-page`)。
- 首次进入某标签才 `src=page`;之后切换只改可见性,**不改 src、不 remove**。
- 宿主容器 `id="viewHost"`(`data-shell`;注入脚本用 `#viewHost` 判定外壳)。
- 激活帧:`visibility:visible; pointer-events:auto`;非激活:`visibility:hidden; pointer-events:none`。
**不用 `display:none`**——部分引擎对 display:none iframe 的定时器/绘制更狠。
- 切换时同步 `localStorage.dcu_tab`(沿用现有键与 `pageToTab` 迁移)。
### 状态保全契约
| 状态 | 保活后 |
|------|--------|
| 功能页内 WS / 轮询 / Module 全局 | 留在各自 iframe 文档内,切走不断 |
| 外壳 `app-lease` | 仍由 `tabs.html` 自持(覆盖整个窗口) |
| `ecat_native` 后台 PDO 循环 | 服务端 + 租约决定,与是否隐藏无关 |
| F5 / ⟳ | **只 reload 激活 iframe**,不重载外壳 |
### 注入刷新脚本(`esp32_server.py` `_RELOAD_SNIPPET`)
- `reloadCur()`:`querySelector('.page-frame.active')`;兼容 `#view`;否则 `location.reload()`。
- 外壳判定:`#viewHost` 存在(兼容旧 `#view`)。
- 功能页内(非 TOP)行为不变。
### 导航与高亮
- 点击标签 → `go(page)`:懒创建 → 激活目标 → 其它帧隐藏。
- iframe `load`:仅当该帧仍是 `activePage` 且 pathname 映射回**本帧登记路径**才写高亮/`dcu_tab`。
页内跳到另一个已登记标签不会自动抢外壳高亮(多帧语义下更安全)。
- 未知页:仍留在当前 iframe 内显示。
### 内存
用户裁定「全部标签常驻」。不引入 LRU 卸载(卸载即回到原病)。
## [S3] Out of Scope
- 不合并成 SPA、不把连接迁到外壳进程
- 不改各页内部轮询/传输实现
- 不改服务端租约协议
- 不做「后台页降频」策略
## Tasks
- [x] T1: `tabs.html` 多 iframe 宿主 + 懒创建 + show/hide 切换 — acceptance: 切走再切回,OTA/电机页的轮询与 WS 仍在,无需重新连接 (covers: S2)
- [x] T2: `esp32_server.py` 注入的 F5/⟳ 只刷新激活帧 — acceptance: 外壳按 F5 仅重载当前标签文档,其它标签 iframe 仍在 DOM 且未 reload (covers: S2)
- [x] T3: 语法/评审验收 — acceptance: py_compile + inline JS node --check PASS;独立评审无 critical (covers: S2)