Day 85 · 边界分析 — 哪些数据归 Vue,哪些留在画布:动手前的设计日
今天的产出不是代码,是一份边界决策文档——把屏 C 的全部数据逐个过堂:归 Vue、归画布、还是走桥。这份文档是本周的施工图(后面六天都按它干活),也是你第一份"框架边界"的设计记录。边界课的全部价值在今天想清楚:动手后返工边界,代价是重来。
目录
一、屏 C 的数据全盘点
先把屏 C 涉及的所有数据列出来(不管归属,先穷举——第 7 周编辑器 + Day 68 展示态的全部状态):
A. 静态结构类
1. 拓扑配置(nodes/edges 声明) —— Day 68 的 topo-config
2. 节点几何(x/y/w/h)
3. 图层尺寸(三层画布的 width/height)
B. 实时数据类(来自 store)
4. 设备状态(ok/warn/danger/offline)
5. 设备温度(节点副标签)
6. 告警列表(连线/节点脉冲的触发源)
C. 交互状态类
7. 视口(scale/offsetX/offsetY)
8. hover 的节点(详情面板数据源)
9. 拖拽中间态(pan 手势的 delta)
D. 动画状态类
10. 告警脉冲相位(0→1 循环)
11. 帧时间戳(rAF 的 now)
二、三问法则逐项过堂
回忆周总览的三问:(1) 需要被 Vue 组件读吗?(2) 每帧都变吗?(3) 变化触发整图重绘还是局部 UI?
| # | 数据 | 问 1(Vue 读) | 问 2(每帧变) | 问 3(重绘/UI) | 裁决 |
|---|---|---|---|---|---|
| 1 | 拓扑配置 | 需要(声明式渲染的源头) | 否 | 加载时整图 | Vue(静态,reactive 安全) |
| 2 | 节点几何 | 否(只有画布用) | 拖拽时高频 | 整图 | 画布(renderer 内部持有) |
| 3 | 图层尺寸 | 否 | resize 时 | 整图 | 画布(resize 协议自治) |
| 4 | 设备状态 | 间接(图例计数) | 否(2s 快照) | 整图重绘 | store → 桥入 |
| 5 | 设备温度 | hover 详情面板要读 | 否 | 局部(副标签)+ hover UI | store → 桥入 |
| 6 | 告警列表 | 详情面板/弹窗要读 | 事件级 | 脉冲动画触发 | store(Vue 侧已有) |
| 7 | 视口 | 否 | pan/zoom 时每帧 | 整图 | 画布(最高频,绝不进 Vue) |
| 8 | hover 节点 | 详情面板是 Vue 组件 | 否(事件级) | 局部 UI(面板内容) | Vue(存 id!见坑 2) |
| 9 | 拖拽中间态 | 否 | 每帧 | 整图 | 画布 |
| 10 | 脉冲相位 | 否 | 每帧 | 局部(动画层) | 画布(rAF 循环私有) |
| 11 | 帧时间戳 | 否 | 每帧 | — | 画布 |
裁决分布的规律:Vue 拿到的全是低频、事件级、需要跨组件消费的;画布私有的全是每帧、高频、纯渲染的。这条规律本身比表格更值钱——记进笔记。
三、桥协议设计
边界定了,桥就是两份显式契约:
3.1 入向桥:Vue → 画布(数据驱动重绘)
/**
* 入向桥契约(Day 87 实现,今天只定义)
* 原则:桥口要"窄"——只传画布需要的最小形态,不传整个 store
*/
interface TopoBridge {
/** 数据快照到达:触发状态层重绘(2s 分频 + 脏检查) */
update(states: Map<string, DeviceState>, temps: Map<string, number>): void;
/** resize 通知(DPR 适配内部处理) */
resize(): void;
/** 动画开关(失活暂停/激活恢复) */
setAnimation(on: boolean): void;
}
3.2 出向桥:画布 → Vue(事件上报)
/**
* 出向桥契约(Day 88 实现)
* 原则:画布"报告事实",不携带渲染对象——传 id 传数据,不传内部节点引用
*/
interface TopoEvents {
/** hover 变化:payload 是设备 id 或 null(离开) */
(e: "hover", id: string | null): void;
/** 节点点击(未来跳详情用,本周预留) */
(e: "select", id: string): void;
}
桥的窄口价值:两份契约就是屏 C 的全部耦合面——未来换渲染引擎(如换 WebGL)只动画布侧,Vue 侧零改动;反之升级 Vue 也不碰画布。边界的本质是可替换性的承诺。
四、反例的代价推演
4.1 如果视口(#7)进了 Vue
pan 手势 → 每帧改 viewport.value(scale/offset)
→ 每帧触发依赖 viewport 的 watch(假设有 HUD 显示缩放比)
→ 每帧 reactive 深检查 + watch 调度 + 组件 patch
→ 与 rAF 循环叠加:60fps × 2 套调度 = 掉帧
修复成本:数据搬家 + 所有消费点重写(本周重来)
4.2 如果 hover(#8)存了节点对象
renderer 命中检测 → 返回内部节点对象 → 存进 hoverNode.value
→ 对象引用每次可能新建(命中检测若返回拷贝)
→ watch hoverNode 每帧误触发 → 详情面板每帧重渲
修复成本:小(改存 id),但定位半天("面板为什么一直闪")
推演的意义:两个反例都不违反"能跑"的标准,但都违反"能扛"的标准——边界设计的失败不在功能,在性能与可维护性的慢性损耗。
五、边界决策文档成文
今天的核心作业——把第二至四节整理成 docs/boundary-screen-c.md 存进 dashboard-vue 工程:
# 屏 C 边界决策文档(Day 85)
## 数据裁决表
(第二节的完整表格,含裁决理由)
## 桥契约
(第三节的两个接口 + 设计原则注释)
## 反例推演记录
(第四节,供未来 review 时对照)
## 复审触发条件
- 设备数 > 200 时:重评"状态层重绘"是否需要脏矩形(第 7 周 Day 46 能力回流)
- hover 详情变复杂(多面板联动)时:考虑 provide/inject 局部上下文
最后一节很重要——边界决策要有失效条件,环境和需求变了边界就要复审,没有一劳永逸的分界线。
六、常见坑点
坑 1:把"边界"当成"墙"
桥走不通时绕路:为了某个小需求直接从画布内部 import store——边界上开洞。纪律:任何数据流动必须走桥契约,桥不够用就扩桥(改接口是显式决策),不开侧门。
坑 2:裁决表写成"感觉"
“这个放画布吧,感觉合适”——没有三问依据的裁决经不起复审。每个裁决必须能回答三问,今天的表格就是范本。
坑 3:静态配置误判为"不用管"
拓扑配置(#1)裁决归 Vue,但它只在加载时消费一次——别顺手做成 computed 派生(无意义的响应式开销)。静态数据用普通常量 + markRaw 亦可,裁决"归 Vue"不等于"必须响应式"。
七、自测挑战
T1 · 数据全盘点 + 三问过堂(60 分钟)
不看本文,自己从零完成第一、二节:先穷举屏 C 全部数据(≥ 10 项),再逐项三问裁决。完成后与本文对照,差异处重点分析——分歧比一致更有学习价值。
T2 · 反例代价推演(30 分钟)
自选一个数据(本文未推演的,如"图例计数"),假设放错边,推演代价链。推演能力是设计能力的预演。
T3 · 桥契约评审(30 分钟)
把第三节的接口给"未来的自己"写注释:每方法的触发时机、频率、payload 形态。写不清楚的地方就是设计漏洞——接口注释是设计的试金石。
八、总结
| 环节 | 要点 |
|---|---|
| 穷举 | 先列全再裁决,防遗漏 |
| 三问 | Vue 读吗 / 每帧变吗 / 触发什么——裁决的唯一依据 |
| 规律 | Vue 拿低频事件级,画布留每帧高频 |
| 桥 | 窄口 + 双向契约 = 可替换性承诺 |
| 复审 | 边界要有失效条件,没有永恒分界线 |
施工图就绪。明天开工:三层画布迁进组件骨架。