美学层:功能全对,页面还是"不对"
这是五层约束系列的第五篇,也是最后一层。前四篇讲了语言层 、样式层、状态层、工程层,母篇是《从能跑到可交付:一套前端 AI 协作系统的设计笔记 》。我用的系统是开源的 frontend-harness-cli ,文中所有规则都能在仓库里找到对应实现。
一个没有报错的页面
先看一个具体的场景。
AI 按需求重写了一个通知列表页。类型检查过,Lint 过,端到端测试全绿,数据一条不差。打开页面:每行挤在一起,未读和已读看不出区别,标题和时间的字号差不多大,列表底部留了一块莫名其妙的空白。
没有任何一处报错。你也挑不出哪行代码"写错了"——间距是它随手写的 margin: 6px,字重是它觉得差不多的默认值。功能全对,页面不对。
前四层管的都是能判定对错的东西:类型对不对、变量用没用、数据准不准、结构合不合规。这一层管的是判定不了的那部分:页面像不像它应该的样子。
为什么这一层只有前端有
通用 Harness 的文献很少讲美学,原因不复杂:多数任务的对错是二值的。代码要么跑通要么跑不通,答案要么对要么错,没有"七分像"这种状态,于是约束层里不需要给"像不像"留位置。
前端有。Omni-I2C 这类图文转代码基准里,当前最强的模型视觉还原能做到七成多,细到参数级的精度只有四成左右,而且实体和布局错误占了所有错误的六成以上。换句话说,模型最常犯的错恰恰不在功能上,在页面长什么样上:元素多一个少一个、位置偏了、间距不对。
所以五层里专门空出一层给它。功能对了页面仍然可能不对,而用户用不用这个产品,很大程度取决于页面对不对——这一层不是装饰,是前端和其它领域分界的地方。
机器怎么查:基线不是摆设
“像不像"要变成能检查的东西,第一步是把"应该的样子"固定下来。我的做法写在栈包的视觉回归一节里,是个很笨的流程:
核心路由先列进这个功能的 pages.json;对着正在运行的构建执行 harness capture,逐个路由截图;再用 harness visual 把新截图和 .harness/baselines/ 里存着的基线图逐像素对比(用的 pixelmatch)。
这里有两个设计决定值得单独说。
没有基线,记为 no-baseline,明确不算通过。 很多视觉回归工具第一次跑会自动把当前截图当基线,于是第一次永远"通过”——哪怕页面是错的,错的样子被当成了标准。我这里反过来:没有基线就是没有基线,这个状态写进报告,谁都看得见,不许它伪装成绿色。
采纳基线是一个明确的、要过评审的动作。 页面改对了,新截图和旧基线必然不同;这时候不是点一下"接受新基线"了事,而是有人看过差异、确认"对,就该长这样",才把新图存为基线。基线是被批准的标准,不是上一次的截图。没有这一步,视觉回归会退化成"和上次一样就行"——上次是错的,就永远错下去。
三条扣分规则,管三种坏事
扫描的界面维度里,和视觉直接相关的有三条规则,各扣 8 分,各管一种不同的坏事:
visual-diff:像素差异率超过允许的上限(maxDiffRatio)。 这是日常的漂移检测:改着改着,页面悄悄变了样。报告不会只给一个"不同",它把选择摆出来——要么这次变化是故意的,那就评审后重新采纳基线;要么 UI 漂移了,回去改代码。含糊的"有点不一样"不许存在。
visual-size-mismatch:截图尺寸和基线对不上。 这一条单独列出来,是因为尺寸变了不是样式漂移,是结构断了:多了一栏、少了一块、断点错位。它和 visual-diff 的处理方式完全不同——漂移是调样式,结构断裂是回去查布局,必须分开报告,不然真正的断裂会被淹没在一堆像素噪声里。
blank-screenshot-evidence:截图文件小得不像一张截图。 评测环节要交截图当证据,但如果页面根本没渲染出来(白屏、构建没起来),截到的就是一张接近空白的图,文件小到异常。这条规则按文件大小识别它,把"空白页当证据交上来"直接判掉。连证据本身都要被查——因为只要证据可以造假,前面所有检查都可以被一张假图糊弄过去。
先有锚点,才谈判断
逐像素对比只能回答"和基线差多少",回答不了"基线本身对不对"。第一次建基线时,凭什么说这个样子是对的?
凭锚点。在澄清阶段,prd.md 里有一节叫"好样子长什么样",vision.md 里存着更完整的版本。它们不许写"简洁现代"这种话,必须写成能量的样子。举个例子,通知列表的锚点是这样写的:
一行一条通知,最新在上;未读行标题加粗、带一个圆点;行间距 12–16px;空列表显示一句话和一个"刷新"按钮。
有了这种锚点,判断才成立:人评审时逐条对,评判者 Agent 打分时也逐条对。没有锚点,“好不好看"就是各说各话——你让一个评审者(人或 AI)对着"简洁现代"打分,他打的是自己的想象,不是你的要求。
所以这一层的顺序是反的:先在文字里把"对"写死,再用像素去量偏差。 锚点是标准,截图是证据,基线是被批准过的标准像。三者缺一个,视觉检查要么没法开始,要么开始了也没有意义。
它只能测一半
这一层必须把边界说到最清楚,因为它最容易被夸大。
自动化能查的是:变量有没有用、作用域对不对、截图和基线像素差多少。查不了的是:颜色搭配和不和谐、间距有没有节奏感、字号层级清不清楚。这些判断目前自动化只能覆盖一半左右,剩下一半要靠人看截图评审,或者靠一个独立的评判者打分——而且那个分数必须刻意往怀疑的方向校准,因为评审者对被评审对象天然宽容,这个毛病在评测阶段那篇里会展开讲。
所以美学层有个尴尬的双重身份:它是五层里最前端特有的一层,也是五层里最弱的一层。前四层给的都是确定性结论,到这一层,有一半结论是"机器说差了多少像素,剩下的人自己看”。承认它弱,比假装它被解决了重要——一个标着"视觉检查通过"的绿色徽章,如果背后只是一次像素 diff,它骗的不只是用户,还有你自己。
五层到此为止
五层约束到这里收尾。回头看这个接力:语言层保证代码形状合法,样式层保证视觉取值不出调色板,状态层保证数据是准的,工程层保证结构和流程合规,美学层保证页面和批准过的样子差得不多。每一层都只保证一件事,每一件都有机器在查——除了美学层的另一半,那一半老实地留给人。
但五层约束管的都是"代码往哪写",管不了一件事"怎么从一句话走到交付"。那是流程的事。下一篇开始讲五个阶段,第一篇是澄清:为什么"完成"必须在写第一行代码之前,就被写成一份可数的清单。