真正进入项目后,我才发现练习代码与生产代码之间并不是规模差异,而是责任差异。练习失败可以重来,生产页面的失败会阻断监控、误导判断,或者让另一个同事无法继续工作。

第一批任务并不复杂:表格、查询条件、趋势图和设备状态。但数据不再像示例那样整齐。字段可能为空,时间格式不一致,接口在不同环境返回不同结构,旧浏览器也会暴露意料之外的问题。

默认值不是容错方案

我曾经为缺失数据加上大量 || '-',页面看起来不报错,却把“没有数据”“接口异常”和“数值为零”混成同一种结果。容错不能只是隐藏错误,它还要保留错误的语义。

后来我会明确区分数据状态,并和接口提供方确认契约。无法恢复的问题应该被记录,可恢复的问题才适合降级展示。

我后来给接口数据加了一层很薄的归一化,不让组件直接猜测原始字段:

type MetricValue =
  | { kind: "value"; value: number; unit: string }
  | { kind: "missing"; reason: "not_reported" | "offline" }
  | { kind: "invalid"; raw: unknown };
 
function normalizeTemperature(raw: unknown): MetricValue {
  if (raw === null || raw === undefined) {
    return { kind: "missing", reason: "not_reported" };
  }
  const value = Number(raw);
  if (!Number.isFinite(value)) return { kind: "invalid", raw };
  return { kind: "value", value, unit: "°C" };
}

渲染 0°C、设备离线和脏数据时,组件会走三条明确分支。更重要的是,invalid 会进入监控,而不是被一个横杠安静吞掉。页面“看起来不报错”不再是唯一目标。

修改之前先理解上下游

生产系统里,很少有真正孤立的一行代码。一个字段同时影响列表、导出、权限和告警。只看当前页面完成需求,可能把问题推到另一个环节。

我开始在动手前追踪数据从哪里来、经过哪些转换、最后被谁使用;提交时说明变化范围,而不是只写“修复 bug”。这些动作没有增加功能,却显著减少返工。

一次字段改名让我吃过亏:列表已经改成新字段,导出仍读取旧字段,页面验收通过后用户才发现 CSV 为空。从那以后,字段级修改我会至少检查四个消费点:显示、筛选、排序、导出。若字段参与告警或权限,再把它们加入清单。

这不是要求每次都全仓库搜索,而是先沿数据契约找消费者。接口类型、转换函数和导出模型如果共享一个稳定入口,检查成本会低很多;如果同一字段在各页面重复解析,问题本身也提示我们缺少边界。

团队代码需要留下理由

生产代码很少只被写一次。临时兼容、特殊阈值和看似多余的判断,如果没有解释,下一位维护者可能“清理”掉它,也可能因为害怕而永远不敢修改。我学会把注释留给无法从代码本身看出的业务原因,并在提交记录里关联问题背景。

与此同时,注释不能替代糟糕结构。能通过命名和拆分表达的意图,应先让代码自己说清楚。文档保存上下文,代码表达当前事实,两者承担不同责任。

生产代码最重要的标准并非聪明,而是可预测。别人能理解它,异常能被发现,修改的影响能被控制。代码从个人作品变成团队资产,就是从这里开始的。

一次生产变更从自检、CI、小流量发布到监控确认的证据链

图:代码进入生产前后都需要可复查的验证结果。

提交前我会自己走一遍失败路径

第一份生产代码之后,我养成一个简单习惯:评审别人之前先评审自己。除了正常输入,还会主动断网、重复点击、让接口返回空数组和 500、刷新页面、用没有权限的账号再走一遍。

我把发现的问题写进 PR:影响面、复现步骤、修复方式和验证命令。这样评审者看到的不只是代码,还能判断我有没有理解它在真实系统里怎样失败。