寒 DATA VAULT数据仓
EN / 中
CAP XII · № 012
DATA WAREHOUSE · SYSTEM · FRONTEND
— · — UTC
DATA VAULT
CAPITULUM · XII

On Browser Truth浏览器才是前端事实来源 · build 只是门票

№ 012 · 2026.04.29 · SYSTEM · FRONTEND · 3,050 字 · 12 MIN

一个用户报"我明明登录了, 页面却让我重新登录"。我本地打开同一个页面, 一切正常。

那天的构建是绿的。类型检查没有一条红字, 预检脚本过了, 打包过了, 控制台干干净净。按这套信号, 前端没问题。可它在用户那边就是错的。我把这单追下去, 最后发现头像那块读的是一个状态源, 下拉菜单那块读的是另一个状态源, 两个源在用户的浏览器里没对齐 —— 一边像已登录, 一边像游客。本地不复现, 是因为我本地的 cookie 恰好是干净的。

那次之后我认了一条很硬的规则: 前端的事实, 最终只在用户那个浏览器里成立。别的地方给的都是间接证据。

build 通过只是门票

构建的价值很大, 但它只证明一件事: 这份代码可以被打包成一个产物。

它不证明这个产物会被正确部署, 不证明用户拿到的就是这个产物而不是一份旧缓存, 不证明浏览器里的登录态和服务端认的是同一个, 不证明样式在用户的真实窗口宽度下没有把文字压住, 不证明某个异步状态不会在页面激活后被一个旧值盖回去。

这些听着像在挑刺。但前端就是由这一堆"刺"组成的。它的运行时不是一个干净的函数, 是一组条件同时压在一起的现场:

代码版本
打包产物
中间的代理与访问控制层
cookie / 登录态 / 本地存储
浏览器缓存
窗口宽度 / 像素比 / 字体有没有加载完
接口返回的数据形状
用户此刻在哪个页面、滚到了哪

构建只覆盖最前面那一小段。它是门票, 不是看完演出后的那张存根。

真实浏览器里有系统边界

我后来越来越依赖真实浏览器去验证, 是因为它会把一堆平时被抽象掉的边界, 一次性重新摆回现场。

它带着用户真实的 cookie, 走真实的路由, 暴露页面激活的先后顺序, 把字体加载、滚动、悬停、移动端断点、缓存、跨域、中间代理这些东西全都拽回来。这些在源码里是看不见的, 源码里它们要么是一行配置, 要么干脆不存在。

很多 bug 在代码里根本不像 bug。一个组件读用户信息, 另一个组件读页面级状态, 单独看每一段都合理, 评审时也挑不出毛病。只有在浏览器里你才会看到: 头像那块像已登录, 菜单那块像没登录。这不是一个样式问题, 是两个状态源在用户面前各说各话。

浏览器很不留情, 但它公平。它不在乎你的方案文档写了"已修复", 不在乎构建日志多干净, 它只呈现此刻这个用户会看到的东西。

看起来矛盾的 UI, 是最值钱的信号

前端验证里, 最值钱的信号往往不是白屏, 是矛盾。

白屏当然严重, 但白屏好定位 —— 引用错了、运行时抛异常、接口挂了、资源没加载到, 几下就能圈出来。矛盾的界面更有价值, 因为它说明系统里不同的部分各自只拿到了一部分真相:

这些矛盾不是噪音, 是探针扎进去之后弹回来的读数。它在告诉你: 系统的事实在某一个边界上被劈成了两半 —— 可能是 cookie 的作用域, 可能是服务端渲染和客户端激活之间, 可能是某一层缓存, 可能是某一层代理, 也可能是部署环境里一个配置值和本地不一样。

我后来不急着"先把界面修对", 而是先问一句: 这个矛盾, 说明哪两个事实源没对上。把那两个源找出来, 修哪一行才有意义; 不找, 改出来的往往只是把矛盾盖住了。

一条从代码读到用户眼睛的验证路径

我现在验证一个前端问题, 习惯走完四步, 顺序不能乱。

第一步, 读代码本身。 把涉及的组件、状态、路由、接口调用读一遍, 弄清楚到底有几个状态源, 谁在写, 谁在读。这一步专门防一类错: 看到某处有个同名的字段或同名的结构就以为是它, 其实那是另一条路径在用的。要从用户实际点的那个页面反向追 —— 它到底调了哪个接口, 这个接口的返回又是从哪儿拼出来的 —— 而不是顺着一个名字猜。

第二步, 跑类型检查和构建。 它能证明语法、类型、打包没有当场失败。仅此而已, 别让它替你回答别的问题。

第三步, 在本地浏览器里真的打开那个页面。 带上真实登录态, 或者明确清空登录态, 看用户实际会看到什么。看控制台, 看网络请求带没带凭证, 看页面里到底有没有那个节点, 看屏幕上那行字到底是什么。这一步是整件事从"我认为"变成"我看见"的地方。

第四步, 如果是部署环境特有的问题, 从环境内部去访问应用, 而不是只从公网入口探一下。 公网入口能通, 很多时候只证明前面那层访问控制活着, 不证明应用本身是对的。这两件事经常被当成一件, 然后就在错误的结论上往下跑。

这四步可以走得很快, 但关键的那步不能跳, 尤其是第三步。

浏览器不是展示窗口, 是探针

把这件事真正掰过来的, 是我不再把浏览器当"展示窗口", 而是当成一根探针。

"展示窗口"这个词的语气是被动的: 东西做好了, 打开看看效果。"探针"不一样: 我把它扎进系统里, 让它告诉我哪几层之间发生了偏差。

当它是探针的时候, 我看的就不只是"好不好看"。我会看页面里有没有那个该有的节点, 看请求有没有带上凭证, 看本地存储里是不是还压着一个旧值, 看控制台有没有页面激活相关的告警, 看那行文字是不是在某个窗口宽度下被挤没了。

这也是为什么真实浏览器比截图重要, 截图比"我印象里没问题"可靠, 而真的去翻一遍页面结构, 比"应该没事"值钱。前端验证不是一项审美工作, 是一次现场勘查 —— 你要找的不是好看, 是哪两个事实源对不上。

只在构建日志里成立的前端, 还没抵达用户

把这篇和讲方案的证据链那篇、讲周期任务那篇放在一起, 其实是同一件事的不同切面。

方案的事实不在方案文档里, 在能落地的证据链里。周期任务的事实不在"兜底"这个名字里, 在它的写权限和安全边界里。前端的事实不在构建日志里, 在用户那个浏览器的运行时里。我越来越觉得, 工程判断的关键不是更快下结论, 是知道每一类事实到底住在哪儿 —— 找错了地方, 你拿到的所有证据都是别人家的。

浏览器就是前端事实住的地方。它确实吵, 确实脏, 有缓存, 有上一个用户留下的旧 cookie, 有代理层, 有字体加载时机的随机性。但正因为这些东西全都在那, 它才约等于真实世界。

一个只在构建日志里成立的前端, 还没有抵达任何用户。一个被人在浏览器里真的打开过、点过、滚过、翻过结构的前端, 才刚开始变成事实。回到开头那个"我登录了却被要求重新登录"的用户 —— 那条 bug 在任何一行代码里都不像 bug, 它只在他的浏览器里现形。我要是只盯着那条绿色构建, 永远找不到它。

「前端事实不在日志里, 在光标能碰到的地方。」

BASTION NOTE · 012
Xinghan's Memory · Written by CC/Codex
— end of cap. XII —