【阶段1收官】day69-monitor-integration

作者:mario 发布时间: 2026-09-03 阅读量:2 评论数:0

Day 69 · 监控页整合与全屏联调 — 最后一块拼图与第一次整机测试

屏 B 的迁移本身不难——第 9 周面板本来就是"订阅 DataSource 渲染"的结构,改成"订阅 Store"只是换数据入口。今天真正的主题是联调:三屏齐活后的整机测试——路由切换与图表 resize 的时序、数据流在轮播下的全链路核查、性能的整体验收。整合项目的 80% 问题都暴露在联调日:单屏都好,合在一起就出幺蛾子。今天按"迁移 → 联调清单 → 性能验收"三步走,明天交付。


目录


一、屏 B 迁移:从 DataSource 到 Store

1.1 改动量盘点

第 9 周 BOSS 战的面板已经是"五模块 + 分频更新"结构(Day 56),迁移改动集中在一处数据入口

// 旧(第 9 周 main.ts):
- const source = new WsDataSource();
- source.subscribe((snap) => scheduler 分发到各模块);

// 新(屏 B 模块):
+ const unsub = store.subscribe({          // 订阅全局 Store
+   onSnapshot: (snap) => kpiCard.update(snap.kpi),      // 原 onKpi
+   onWindow:  (windows) => lineChart.update(windows),    // 曲线改吃窗口(见第二节)
+   onAlarms:  (alarms) => alarmList.update(alarms),
+ });

MessageScheduler 不再由屏 B 自己持有——它的职责已被 Store 吸收(Day 65 第四节的三层分频表:scheduler 在 WsDataSource 内部、Store 管订阅分频、屏内模块分帧保留)。这是本周唯一一次"职责搬家",值得在笔记里记一笔:整合不是搬运代码,是重新分配职责

1.2 屏 B 的标准模块骨架

/**
 * 屏 B:实时监控(第 9 周面板迁移版)
 */
export function createMonitorScreen(
  el: HTMLElement,
  store: ScreenStore
): ScreenModule {
  // 五卡片布局(Day 56 的 grid 迁移 + Day 66 的 TechCard 包装)
  const lineCard   = createTechCard("实时温度监控");
  const gaugeCard  = createTechCard("关键设备仪表");
  const kpiCardBox = createTechCard("关键指标");
  const rankCard   = createTechCard("车间能耗排名");
  const radarCard  = createTechCard("设备健康度评估");
  el.append(
    wrap(kpiCardBox.root, "monitor__kpi"),
    wrap(lineCard.root,   "monitor__line"),
    wrap(rankCard.root,   "monitor__rank"),
    wrap(radarCard.root,  "monitor__radar"),
    wrap(gaugeCard.root,  "monitor__gauge"),
  );

  // 五模块(第 8 周的图表模块原样迁移)
  const kpi    = createKpiCard(kpiCardBox.body);
  const line   = createLineChart(lineCard.body, ["1号炉", "2号炉"]);
  const gauges = createGaugeRow(gaugeCard.body);
  const rank   = createRankChart(rankCard.body);
  const radar  = createRadarChart(radarCard.body);

  let unsub: (() => void) | null = null;
  return {
    activate() {
      unsub = store.subscribe({
        onSnapshot: (s) => {
          kpi.update(s.kpi);
          gauges.update(s);         // 内部自带动画节流(Day 56 坑 3)
        },
        onWindow: (windows) => line.update(windows),
      });
      // 轮播切回时容器尺寸恢复,保险 resize(Day 52 坑 1 的轮播版)
      resizeAll([line, gauges, rank, radar]);
    },
    deactivate() {
      unsub?.();
      // 非激活屏停图表更新但保留实例(切回秒恢复——Day 65"激活喂"策略)
    },
  };
}

二、迁移中唯一的结构性改动:appendBatch 的窗口来源

第 9 周曲线模块自己维护 buffers(定长窗口);现在 Store 已经维护了 tempWindows(Day 65),两边重复。改造原则:状态只存一份——曲线模块从"自己攒数据"改为"消费 Store 窗口":

/**
 * 曲线模块改造:数据源从"自己缓冲"变为"Store 窗口"
 * 好处:屏 B 隐藏期间窗口照常滚动(Store 一直吃数据),
 *       切回来时立刻是最新 120 点——不需要任何补拉
 */
export function createLineChart(
  el: HTMLElement,
  deviceNames: string[]
): { update(windows: Map<string, { ts: number; v: number }[]>): void; resize(): void } {
  const chart = echarts.init(el, "industrial-dark");

  chart.setOption({
    grid: { left: 50, right: 20, top: 40, bottom: 30 },
    tooltip: { trigger: "axis" },
    legend: { top: 5 },
    xAxis: { type: "time" },
    yAxis: { type: "value", min: 60, max: (v) => Math.max(100, v.max + 5) },
    series: deviceNames.map((name) => ({
      type: "line" as const, name,
      showSymbol: false, smooth: true, animation: false,
      data: [] as [number, number][],
      markLine: { /* Day 56 的告警阈值线原样迁移 */ },
    })),
  });

  return {
    update(windows) {
      chart.setOption({
        series: deviceNames.map((name, i) => ({
          data: (windows.get(DEVICE_ID[i]) ?? []).map((p) => [p.ts, p.v] as [number, number]),
        })),
      });
    },
    resize() { chart.resize(); },
  };
}

🎯 这就是 Store 存在价值的具象化:“隐藏期间数据也在长”——第 9 周的面板切走再切回,曲线会断档;今天切回即是满窗。一行架构决策换来的体验差。


三、联调清单一:路由与渲染时序

三屏全活后第一个幺蛾子高发区:切屏时序。逐项验证:

□ 切到屏 B:五卡片入场动效(Day 67)+ 图表 resize 都发生且只发生一次
□ 快速连切(1→2→3 连点):无白屏、无半初始化图表
□ 首次启动:屏 A 先入场,屏 B/C 在隐藏状态完成 ECharts init
   (验证点:切到 B 时不是从零 init——首切零等待)
□ 轮播一整圈(A→B→C→A):帧率曲线平稳,无周期性尖峰

关键实现:init 时机与 resize 时机

// 屏模块构造即 init(隐藏状态下容器有尺寸——Day 64 选 opacity 的回报)
const chart = echarts.init(el, "industrial-dark");
// activate 时 resize(从 opacity:0 回来,理论上尺寸没变,防御性调用)
resizeAll(charts);

四、联调清单二:数据流全链路核查

拿一个具体数字做"全链路追踪"——联调最有效的笨办法

核查对象:1 号炉温度,从服务器到三个屏的呈现

服务器端:  generateSnapshot → temp(80) = 85.3
WS 帧:      {"type":"snapshot","seq":1234,"payload":{devices:[{deviceId:"f1",...85.3}]}}
传输层:     ReconnectingWebSocket onmessage → heartbeat.feed
会话层:     SessionManager → seq(1234) > lastSeq ✅ → 投递
数据源:     WsDataSource → scheduler.push
调度层:     MessageScheduler rAF 批 → dispatch
Store:     ingest → latest 更新 + tempWindows["f1"] push
屏 A:       onSnapshot → KPI 翻牌 → 显示 85.3 相关 KPI ✅
屏 B:       onWindow → 曲线最新点 (ts, 85.3) ✅ / gauge 指针 85.3 ✅
屏 C:       onSnapshot(2s 分频) → 拓扑节点副标签 85.3℃ / 状态色 warn ✅

在 DevTools 里沿这条链打 5 个断点跑一遍,确认每个环节的值和时延——任何一环对不上就是架构被绕过了(比如某屏偷偷自己连了 WS)。


五、联调清单三:容错场景在轮播下的表现

第 9 周验证过单页容错,今天验证轮播叠加容错

场景 1:切到屏 B 的瞬间杀服务器
□ 状态灯变黄(顶栏组件,全屏共享)
□ 屏 B 曲线冻结、无报错刷屏
□ 轮播继续切 A/C(连接断不影响路由)
□ 服务器重启 → 退避重连 → 三屏数据同帧恢复

场景 2:屏 C 激活时断网 30 秒
□ 补传触发时屏 C 可能已切走——验证补传数据进 Store 后
   切回屏 C 时窗口是补全的(第二节的"隐藏期间数据也在长")

场景 3:告警发生在屏 B 激活时
□ 屏 A(隐藏)的告警列表同步更新(Store 事件不分屏激活与否)
   ——验证:切到屏 A 时告警已在列表里,不是"切过去才开始拉"

⚠️ 场景 3 是最容易被忽略的:如果告警列表只在 activate 时拉取,切屏就会丢告警——事件必须由 Store 主动推给所有订阅者,而不是屏幕激活时拉。Day 65 的订阅模型天然正确,今天只是验证。


六、整机性能验收

场景 指标 目标 实测记录
单屏稳态 ×3(各 10 分钟) fps ≥55
轮播整圈循环 ×10 fps ≥55
全动效开启(扫光+雷达+脉冲) fps ≥55
30 分钟整机 JS Heap 稳定
三屏 ECharts 实例 + Canvas 内存 无泄漏趋势
页面 hidden 10 分钟 CPU ≈0

内存泄漏的重点排查位

1. 切屏 50 次:订阅是否泄漏(deactivate 退订失败 → subs 数组膨胀)
   → Store 加临时日志:subs.length 应稳定在个位数
2. 轮播与告警叠加:alarm-row DOM 是否只增不减(超上限截断)
3. ECharts 实例数:应为固定值(五模块×1 + 无游离实例)
   → echarts.getInstanceCount 或遍历 registry 验证

七、常见坑点

坑 1:切屏白屏一闪

隐藏屏的入场动效从 opacity:0 起播,与切屏淡入叠加出现"双重渐入"。修复:切屏入场动效只在首次播(Day 67 的 2.3 纪律),轮播切屏只走 router 的 opacity 过渡。

坑 2:屏 B 切回来曲线闪一下全量动画

animation: false 在某次 setOption 里被覆盖(merge 进了带动画的 option)。修复:检查 Day 54 的优化配置没有被迁移时丢掉。

坑 3:三个屏的 onSnapshot 收到不同频率

屏 A 设了 5s、屏 B 默认 1s、屏 C 2s——这是设计不是 bug(分频);但如果发现同屏内 KPI 和 gauge 数据不一致(差了 4 秒),是分频粒度不一致的体验问题。修复:同屏内模块用同一订阅(1.2 的实现已保证:一个 subscribe 喂全屏)。

坑 4:内存缓慢上涨,最后定位到订阅泄漏

某屏的模块在 resize 时重新订阅而未退订旧订阅。修复:订阅只在 activate/deactivate 成对出现,模块内部禁止二次 subscribe。

坑 5:快速连切时出现两个"半透明屏"

CSS 过渡未完成时再次切屏,两个屏都处于中间态。修复:router 的 switchTo 里先 classList.remove 清理上一轮过渡(或用 transitionend 门闩),最简单方案:切换时给容器加 no-transition 一帧。


八、自测挑战

T1 · 屏 B 迁移 + 三屏联调清单全过(80 分钟)

第一至五节全部清单逐项打勾,任何一项不过先修再继续。今天核心作业

T2 · 全链路追踪报告(30 分钟)

按第四节做一次温度值的 5 断点追踪,把每环节的值和时延记成表格进博客——联调方法论的可展示证据

T3 · 切屏压力测试(20 分钟)

脚本或手动连点切屏 100 次:无白屏、无报错、subs.length 稳定、ECharts 实例数不变。

T4 · 泄漏 hunting(进阶,40 分钟)

用 Memory 面板做三快照对比(启动后 / 轮播 10 圈后 / 再 10 圈后):Detached DOM 和 ECharts 内部对象无增长趋势。发现泄漏修到零增长为止——这 40 分钟是面试聊内存管理时的全部底气


九、总结

环节 要点
屏 B 迁移 数据入口换 Store;窗口状态上移(隐藏期间数据也在长)
职责搬家 scheduler 分频职责归 Store,屏内只留模块分帧
路由时序 隐藏态 init + 激活 resize + 快速连切防护
全链路核查 一个值五个断点,验证架构没被绕过
轮播容错 事件推送不分激活与否;补传与切屏解耦
性能验收 三屏稳态/轮播/全动效三档 fps + 泄漏清零

整机就绪。明天交付日:最终验收、演示视频、README、简历条目、第 2 个月毕业总结——把两周的心血变成拿得出手的资产。

评论