Day 69 · 监控页整合与全屏联调 — 最后一块拼图与第一次整机测试
屏 B 的迁移本身不难——第 9 周面板本来就是"订阅 DataSource 渲染"的结构,改成"订阅 Store"只是换数据入口。今天真正的主题是联调:三屏齐活后的整机测试——路由切换与图表 resize 的时序、数据流在轮播下的全链路核查、性能的整体验收。整合项目的 80% 问题都暴露在联调日:单屏都好,合在一起就出幺蛾子。今天按"迁移 → 联调清单 → 性能验收"三步走,明天交付。
目录
- 一、屏 B 迁移:从 DataSource 到 Store
- 二、迁移中唯一的结构性改动:appendBatch 的窗口来源
- 三、联调清单一:路由与渲染时序
- 四、联调清单二:数据流全链路核查
- 五、联调清单三:容错场景在轮播下的表现
- 六、整机性能验收
- 七、常见坑点
- 八、自测挑战
- 九、总结
一、屏 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 个月毕业总结——把两周的心血变成拿得出手的资产。