From eaafee131211190928e0c035f7c0fd89ed722fd4 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 16 Sep 2026 02:37:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(shell):=20tabs=20=E5=A4=9A=20iframe=20?= =?UTF-8?q?=E5=B8=B8=E9=A9=BB=E4=BF=9D=E6=B4=BB=EF=BC=8C=E5=88=87=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E4=B8=8D=E5=86=8D=E9=94=80=E6=AF=81=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单 iframe 换 src 会卸载文档,WebSocket/轮询/传输状态全灭。 改为一标签一懒创建 iframe,切换只改 visibility;F5/⟳ 只 reload 激活帧。 --- README.md | 4 +- docs/compose/spec/tab-keepalive.md | 76 +++++++++++++++++++++++++++ esp32_server.py | 18 +++++-- tabs.html | 83 ++++++++++++++++++++++++------ 4 files changed, 157 insertions(+), 24 deletions(-) create mode 100644 docs/compose/spec/tab-keepalive.md diff --git a/README.md b/README.md index 37fb37a..e94ccfc 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ esp32_app.vbs esp32_web.vbs serve.bat 三个启动入口 ## 页面 -`tabs.html` 是单窗口多标签外壳,用 iframe 承载下列功能页,激活标签记忆在 -`localStorage` 的 `dcu_tab`(按文件名匹配,与目录层级无关): +`tabs.html` 是单窗口多标签外壳,用**多 iframe 常驻保活**承载下列功能页:每个标签一个懒创建 iframe,切换只改可见性、不销毁文档(否则页内 WebSocket/轮询会随导航全灭)。激活标签记忆在 +`localStorage` 的 `dcu_tab`(按文件名匹配,与目录层级无关;启动只预载该标签): | 页面 | 用途 | 主要脚本 | |------|------|----------| diff --git a/docs/compose/spec/tab-keepalive.md b/docs/compose/spec/tab-keepalive.md new file mode 100644 index 0000000..f47ca9c --- /dev/null +++ b/docs/compose/spec/tab-keepalive.md @@ -0,0 +1,76 @@ +--- +feature: tab-keepalive +status: designed +updated: 2026-09-16 +branch: feat/tab-keepalive +commits: +--- + +# Tab Keep-Alive(多 iframe 常驻) + +## Report + +## [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 的定时器/绘制更狠;`visibility` + 绝对定位铺满即可。 +- 切换时同步 `localStorage.dcu_tab`(沿用现有键与 `pageToTab` 迁移)。 + +### 状态保全契约 + +| 状态 | 保活后 | +|------|--------| +| 功能页内 WS / 轮询 / Module 全局 | 留在各自 iframe 文档内,切走不断 | +| 外壳 `app-lease` | 仍由 `tabs.html` 自持(覆盖整个窗口) | +| `ecat_native` 后台 PDO 循环 | 服务端 + 租约决定,与是否隐藏无关 | +| F5 / ⟳ | **只 reload 激活 iframe**,不重载外壳(否则全部保活帧被销毁) | + +### 注入刷新脚本(`esp32_server.py` `_RELOAD_SNIPPET`) + +- `reloadCur()`:优先 `document.querySelector('.page-frame.active')`;无则退化 `location.reload()`。 +- 外壳判定:`#viewHost` 存在(替代原 `#view` iframe 的语义)。 +- 功能页内(非 TOP)行为不变:去掉悬浮钮,覆写 `_blank`。 + +### 导航与高亮 + +- 点击标签 → `go(page)`:确保帧存在 → 激活目标 → 其它帧隐藏。 +- iframe `load`:若 pathname 映射到某 TAB,更新外壳高亮与 `dcu_tab`; + 不因非激活帧的延迟 load 误抢激活(仅当该帧已是 active 才写 `dcu_tab`)。 +- 未知页(页内跳到未登记 html):仍留在当前 iframe 内显示,外壳不高亮新标签。 + +### 内存 + +用户裁定「全部标签常驻」。6 个功能页同时活着的代价可接受; +不引入 LRU 卸载(卸载即回到现在的病)。 + +## [S3] Out of Scope + +- 不合并成 SPA、不把连接迁到外壳进程 +- 不改各页内部轮询/传输实现 +- 不改服务端租约协议 +- 不做「后台页降频」策略(浏览器 Page Visibility 自带节流即可) + +## Tasks + +- [ ] T1: `tabs.html` 多 iframe 宿主 + 懒创建 + show/hide 切换 — acceptance: 切走再切回,OTA/电机页的轮询与 WS 仍在,无需重新连接 (covers: S2) +- [ ] T2: `esp32_server.py` 注入的 F5/⟳ 只刷新激活帧 — acceptance: 外壳按 F5 仅重载当前标签文档,其它标签 iframe 仍在 DOM 且未 reload (covers: S2) +- [ ] T3: 语法/手工验收路径 — acceptance: `node --check` 若适用;人工路径:开 OTA→电机→切回 OTA,指标不全灭 (covers: S2) diff --git a/esp32_server.py b/esp32_server.py index 87b0b65..f8d656a 100644 --- a/esp32_server.py +++ b/esp32_server.py @@ -61,7 +61,8 @@ g_mutex = None # 注入脚本对三种上下文自适应: # 1) iframe 内的功能页(标签外壳承载): 移除悬浮按钮, 但仍覆写 window.open/_blank # 让页面里的"新窗口打开"在 iframe 内跳转, 不弹系统浏览器; -# 2) 顶层标签外壳(tabs.html, 含 #view iframe): ⟳ 刷新当前 iframe, 不显示 ⌂; +# 2) 顶层标签外壳(tabs.html, 含 #viewHost 多 iframe 宿主): ⟳ 只刷新激活的 +# .page-frame, 其它标签保活帧不销毁、不 reload; # 3) 顶层独立页(浏览器直开某 html): ⟳ 刷新自身, 非主页时显示 ⌂ 回到标签外壳。 _RELOAD_SNIPPET = """ @@ -60,7 +75,8 @@
⟳ 刷新当前页 · F5
- + +