← 学习路线

验收:怎么确认"真的没变"

迁移或重构之后,怎样证明外观真的没变?截图比对报出了差异,怎么判断是网站变了,还是测量错了?

课型:流程·约 25 分钟·4 个术语

先看出事的那一刻

2026 年 9 月 27 日,这个网站从 React 迁到 Astro。站长只提了一个要求:外观不能变。AI 把验收标准定成"新旧两版逐屏截图、逐像素比对":在本地同时跑起旧版和新版,用同一个浏览器截图,把两张图的像素逐个相减,算出不同像素占多少。

第一轮结果:列表页 0.000%,文章页 0.01% 左右,首页 4.3%。改了截图方法之后,第三轮更糟:列表页 8.3%,一篇文章 7%。两次,AI 都差一点把这些数字当了真。

这节课要回答的是:迁移或重构之后,怎样证明外观真的没变?截图比对报出了差异,怎么判断是网站变了,还是测量错了?

装备几个词

为什么要懂

AI 替你做了

AI 能写出截图脚本和比对脚本,几分钟跑完十几组页面,给你一张全是百分比的表。

留给你的

先确认测量本身可信,再相信它给的数字;还要看清楚比了哪些页面,哪些没比。

不懂的代价

为测量误差去改代码,越改越偏;或者反过来,看到一个 0% 就以为整个网站都没变。

差异是从哪来的

四轮比对,结果是这样的:

轮次截图方法首页列表页两篇文章
1整页截图,准备脚本没等跑完4.341%0.000%0.011% / 0.025%
2先分段滚动一遍,再等 2 秒0.000%0.000%0.011% / 0.025%
3改成逐屏截图,漏了"立即滚动"4.036%8.331%7.003% / 3.901%
4逐屏截图,加回"立即滚动"0.000%0.000%0.014% / 0.028%

第 1 轮和第 3 轮的差异,来自两个不同的原因:

  • 没等准备好,就按了快门。截图前,脚本要先把页面滚一遍、等字体、再等 1.5 秒。可截图工具执行这段脚本时并不等它跑完,截到的列表还在淡入:三行的透明度分别是 0.996、0.958、0.883。
  • 页面还在滑动。网站开着平滑滚动。逐屏截图时让页面滚到下一屏,截图那一刻它还在半路上。第 1 轮的脚本写了"立即滚动",第 3 轮重写时漏掉了。

两次的差异都来自测量方法,不是网站本身。先用同一个页面连续截两次、互相比对,就能发现这一点:同一个网站和自己比,差异也不是 0。

剩下的那一点差异

测量修好之后,文章页还剩 0.01% 到 0.03% 的差异。打开对应的位置一看,是旧网站的两个 bug:

  • 没写语言的代码块,被当成了行内代码的样式:第一行缩进,还带着底色。
  • 有序列表丢了起始编号:一篇文章里的第 26 到 37 题,在旧网站上显示成了 1 到 12。

新网站两处都渲染对了,这两处差异就保留了下来。这就是逐像素比对的价值:人眼看不出的 0.01%,藏着两个存在了很久、却没人发现的 bug。

0% 说明了什么,没说明什么

最后的验收结论,要把三栏都写出来:

  • 比了什么:首页、列表页和 3 篇文章;亮色、暗色、中英文、手机宽度,共 13 组。中文界面下首页和列表页 0.000%,文章在 0.03% 以内。
  • 没比什么:其余 22 篇文章;404 页只核对了文字。
  • 有意保留的差异:英文界面把网页的语言标记改成了英文,破折号换了字形,列表页因此短了 15 像素;以及上面那两个旧 bug 的修正。

只写一句"差异 0.000%",读的人会以为整个网站都比过了。

深潜工具自己的脾气+

这次验收里,工具本身的特点也造成过误判:

  • 截图工具执行一段包含等待的脚本时,并不等它结束就返回了,脚本里的"等图片、等字体"其实从没生效。
  • 整页截图的长边被限制在 2000 像素:一篇长文章被缩成原来的十分之一左右再比对,细节全没了。改成逐屏截图,才看得清。
  • 无界面浏览器和真实浏览器的表现也不总一样:同一段清理网址的脚本,在真实浏览器里生效,在无界面浏览器里却看不出效果。

所以动手比对之前,先用一个已知结果的例子试一遍工具:同一个页面截两次、比一次,应该是 0。

找茬

下面是第 1 轮的截图脚本和比对脚本(节选)。截图工具执行 js 时不会等待异步脚本结束,整页截图的长边会被压到 2000 像素以内。找出让测量本身不可信、或者让结果说不清楚的地方。

这段 Shell / JS / Python 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。

查看代码与答案(5 处问题)
 1  PREP='(async () => {
 2    document.querySelectorAll("canvas").forEach(c => c.style.visibility = "hidden");
 3    for (let y = 0; y <= H; y += 300) { window.scrollTo({ top: y, behavior: "instant" }); await new Promise(r => setTimeout(r, 60)); }
 4    await document.fonts.ready;
 5    await new Promise(r => setTimeout(r, 1500));
 6    return "ready " + document.documentElement.scrollHeight;
 7  })()'
 8  $B js "$PREP" >/dev/null
 9  $B screenshot "$OUT/$tag-$name.png" >/dev/null
10  
11  # diff.py(逐像素比对,节选)
12  d = ImageChops.difference(pad(a), pad(b)).convert("L").point(lambda v: 255 if v > 24 else 0)
13  ratio = sum(1 for v in d.getdata() if v) / (w * h)
  • 第 2 行 · 低危 · 故意不比的部分,要写进报告:页面背景是一张随机生成的画布,每次都不一样,把它藏起来再比是合理的。但这等于在比对里挖了一个盲区:验收报告里要写明"背景画布没有比"。 该问的话:这次比对故意排除了哪些部分?报告里写了吗?
  • 第 8 行 · 高危 · 没等准备脚本跑完,还扔掉了“准备好了”的信号:这段准备脚本是异步的,截图工具执行它时并不等待;最后返回的"ready"本来是唯一能证明它跑完的信号,又被 >/dev/null 扔掉了。结果截到的是还在淡入的页面。 该问的话:截图的那一刻,你怎么确认页面已经准备好了?
  • 第 9 行 · 中危 · 整页截图被缩小了:这个工具会把整页截图的长边压到 2000 像素以内。一篇长文章被缩到原来的十分之一左右,细小的差异在缩小时就被抹平了。 该问的话:截图是原始分辨率吗?一篇长页面被缩小了多少?
  • 第 12 行 · 低危 · 只比亮度,漏掉颜色的变化:先把差异图转成灰度,再用 24 作阈值。颜色变了、但亮度差不多的地方,可能被判成"一样"。比对颜色时,最好分通道看。 该问的话:只改了颜色、亮度差不多的地方,这个比对能发现吗?
  • 第 13 行 · 中危 · 一个百分比,看不出差异在哪:整页只算出一个比例。0.011% 看起来可以忽略,可它恰好是一个真实的旧 bug。报告里要附上差异的位置,最好是一张标出不同之处的图。 该问的话:这 0.01% 的差异具体在页面的哪个位置?

快测

1. 截图比对报告首页差异 4%,可你肉眼看不出任何区别。最先该怀疑什么?

查看选项与答案
  • A. 截图的时候页面是不是还在动:动画、字体加载、平滑滚动(正确)——同一个页面连续截两次、互相比对,就能检验测量本身稳不稳。
  • B. 新版确实改动了页面,要按 4% 去逐段定位并改回旧样子——先确认测量可信。这次有两轮差异都来自截图方法,不是网站;为测量误差去改代码,越改越偏。
  • C. 比对脚本的算法有误,换一个更精确的工具再比一次——换工具解决不了截图时机的问题:截到的是还在淡入、还在滚动的页面,换什么工具比,差异都还在。

先让测量可信,再相信它给的数字。

2. 比对结果全是 0.000%。能说明网站完全没变吗?

查看选项与答案
  • A. 说明比对脚本有问题,出现 0.000% 本身就不正常——0% 是可能的:这次第四轮里,首页和列表页就是 0.000%。前提是测量可信、范围写清。
  • B. 能,逐像素比出 0%,就说明每个像素都一模一样——比对时先转成灰度、再用阈值过滤,颜色变了但亮度差不多的地方,也会被判成一样;而且它只覆盖比过的那些页面和状态。
  • C. 不能,只说明比过的页面和状态没变,没比的要另外交代(正确)——这次 25 篇文章只抽查了 3 篇,报告里要写清楚比了什么、没比什么。

一个不带范围的 0%,会让人以为什么都比过了。

3. 测量修好之后,还剩 0.01% 的差异。最该怎么处理?

查看选项与答案
  • A. 把新网站改得和旧网站一模一样,直到差异降为 0——这两处差异是旧网站的 bug,新网站渲染对了。把对的改回错的,只为让数字好看,不值得;保留下来,写进“有意保留的差异”就行。
  • B. 设一条容忍线,低于 0.05% 的差异一律先放过——这次的 0.01% 里,藏着旧网站的两个真实 bug。一条统一的容忍线,会把它们一起放过去。
  • C. 打开差异所在的位置看一眼,再决定是修正、保留还是忽略(正确)——看过才知道:这次的 0.01% 里,藏着两个存在了很久、却没人发现的旧 bug。

小差异不是可以忽略的理由,而是值得看一眼的线索。

判断时刻

迁移完成。截图比对:中文界面下首页和列表页 0.000%,抽查的 3 篇文章在 0.03% 以内,英文界面的列表页差异 4.9%。你要写验收结论。

你会怎么写?

三个选项各自的代价
  • A. 写“外观完全一致”,合并上线——考察只报好消息:最简单,但读的人会以为 25 篇文章、所有界面都比过了。英文界面那 4.9% 是怎么回事,也不会再有人去问。
  • B. 写清楚比了什么、没比什么、哪些差异是有意保留的,再上线——考察结论带着范围:多写几行:13 组比对,只抽查了 3 篇文章;英文界面因为语言标记变化,列表页短了 15 像素,有意保留;还顺带修正了两个旧 bug。读的人知道这个 0% 覆盖到了哪里。
  • C. 先把剩下 22 篇文章全部比完再说——考察全量验证:最稳,但要多花不少时间。如果文章页都出自同一个模板,抽查几篇、再比对一次全部构建产物,往往就够了;全量截图更适合模板多、改动大的情况。

验收结论要带着范围:比了什么、没比什么、哪些差异是有意的。一个不带范围的 0%,比一个说清楚了的 4.9% 更危险。

带走

下次让 AI 做这件事时,问它

  1. 这次验收要比哪些页面、哪些状态(亮暗、中英、手机宽度)?哪些不比,为什么?
  2. 截图之前,动画、字体加载、平滑滚动都处理了吗?你怎么确认页面已经准备好了?
  3. 每一组差异具体出现在页面的哪个位置?请截出来给我看。
  4. 验收报告里,把比过的、没比的、有意保留的差异分三栏写清楚。

自己验证

  • 差异不为零时,先把两张截图并排打开,找到差异的具体位置,再下结论。
  • 同一个页面连续截两次、互相比对:如果差异不是 0,说明测量本身就不稳定。
  • 随机挑几个不在清单里的页面,亲手打开看一眼。

先让测量可信,再相信它给的数字;结论要带着范围。