技能:视觉差异对比
比较两个 URL 的视觉呈现——基准(如生产站点)和当前(如本地开发服务器或 Storybook 故事)——以发现像素级差异并指导修正。
何时使用
- 在 Storybook 或 Astro 站点中复制现有页面或部分
- 验证组件相对于参考设计的视觉准确性
- 样式更改后检查跨断点的响应式保真度
- 迭代 CSS/HTML 直到组件与其参考匹配
何时不使用
- 仅内容更改(文本更新、文字编辑),布局不变
- 比较来自 Figma 的设计——改为使用 Figma MCP 技能
- 性能或可访问性审计——使用 Lighthouse 或 a11y 插件
前提条件
开始前,请确认:
- Playwright MCP 已在
.vscode/mcp.json中配置,带有--caps=vision - pixelmatch 和 pngjs 已安装:检查
package.json的 devDependencies - 基准 URL 可访问(外部站点或正在运行的服务器)
- 当前 URL 可访问(
:4321上的开发服务器、:6006上的 Storybook 等) - 工作区根目录中存在
temp/目录(它已被 git 忽略)
如果开发服务器或 Storybook 未运行,请使用 VS Code 任务启动它们:
- 开发服务器:使用 Dev Server 任务
- Storybook:使用 Storybook 任务
工作流程
步骤 1:接收输入
代理需要:
| 输入 | 必需 | 示例 |
|---|---|---|
| 基准 URL | 是 | https://www.bitovi.com/ |
| 当前 URL | 是 | http://localhost:4321/ 或 http://localhost:6006/?path=/story/card--default |
| CSS 选择器 | 否 | #hero-section 或 .card-grid(用于元素范围比较) |
| 断点 | 否 | 默认为移动端 (375×667)、平板 (768×1024)、桌面 (1280×720) |
步骤 2:截取基准截图
组件范围比较(比较单个组件时首选)
当比较特定组件时——特别是当 Storybook 故事仅渲染该组件,但基准页面包含其周围的完整页面时——裁剪到组件的边界框,以便差异不会被不相关的页面部分(导航栏、粘性覆盖层、上下方的其他模块)污染。
如何在基准页面上裁剪:
- 导航到基准 URL
- 将视口大小调整为
{ width: 1280, height: 720 } - 将目标元素滚动到视图中:
() => document.querySelector('<selector>').scrollIntoView({ behavior: 'instant', block: 'start' }) - 测量组件的渲染高度:
() => document.querySelector('<selector>').getBoundingClientRect().height - 将视口高度调整为该确切高度(保持宽度不变):
mcp_playwright_browser_resize → { width: 1280, height: <measured height> } - 截图——现在只捕获组件区域。
背景溢出警告: 组件背后可见的内容在 Storybook(空白)和基准页面(其他模块)之间会有所不同。常见情况:覆盖顶部的粘性/本地导航栏、显示背后其他页面内容的模态框/覆盖层以及部分透明的组件。这些残留的差异聚集在组件自身内容之外,无需处理——忽略它们。
如何在 Storybook 故事上裁剪:
Storybook 的 iframe.html 仅渲染组件,没有周围的 chrome——无需调整大小。只需导航并截图:
mcp_playwright_browser_navigate → { url: "http://localhost:6006/iframe.html?id=<story-id>&viewMode=story" }
mcp_playwright_browser_resize → { width: 1280, height: <same height as baseline crop> }
mcp_playwright_browser_take_screenshot → { filename: "temp/vdiff-current-desktop.png" }
整页视口截图(比较整页时)
1. mcp_playwright_browser_navigate → { url: "<baseline-url>" }
2. mcp_playwright_browser_resize → { width: 1280, height: 720 }
3. mcp_playwright_browser_wait_for → { time: 2 } # 让页面完全渲染
4. mcp_playwright_browser_take_screenshot → {
type: "png",
filename: "temp/vdiff-baseline-desktop.png"
}
针对每个断点重复:
| 断点 | 宽度 | 高度 | 文件名后缀 |
|---|---|---|---|
| 移动端 | 375 | 667 | -mobile |
| 平板 | 768 | 1024 | -tablet |
| 桌面 | 1280 | 720 | -desktop |
步骤 3:截取当前截图
对当前 URL 重复完全相同的过程,使用 temp/vdiff-current-{breakpoint}.png 文件名。
重要: 在两侧使用相同的视口尺寸和相同的裁剪策略(组件范围或整页),以便进行比较。
步骤 4:运行差异脚本
针对每个断点,在终端中运行 visual-diff 脚本:
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-desktop.png \
--current temp/vdiff-current-desktop.png \
--output temp/vdiff-diff-desktop.png
脚本将 JSON 输出到 stdout:
{
"totalPixels": 921600,
"diffPixels": 4521,
"diffPercent": 0.49,
"width": 1280,
"height": 720,
"baselineDimensions": { "width": 1280, "height": 720 },
"currentDimensions": { "width": 1280, "height": 720 },
"dimensionsMismatch": false,
"diffImagePath": "/absolute/path/to/temp/vdiff-diff-desktop.png"
}
如果 dimensionsMismatch 为 true,则图像尺寸不同。脚本在比较前会用透明像素填充较小的图像。这通常表明存在需要调查的布局问题。
步骤 5:查看差异图像
启动一个临时 HTTP 服务器来提供 temp/ 目录,然后导航 Playwright 到差异图像:
# 在后台启动服务器(每个会话运行一次)
npx -y http-server temp/ -p 8787 --cors -c-1 &
然后导航 Playwright 到差异图像:
mcp_playwright_browser_navigate → { url: "http://localhost:8787/vdiff-diff-desktop.png" }
mcp_playwright_browser_take_screenshot → {} # 触发视觉——代理现在可以看到差异
Playwright MCP 上的 --caps=vision 标志意味着代理可以直接在屏幕截图输出中看到图像。
注意: file:// URL 被 Playwright MCP 阻止,因此 HTTP 服务器是必需的。启动一次并在会话中为所有差异图像重复使用。
解读差异图像:
- 红色像素 = 基准与当前之间的差异
- 黄色像素 = 抗锯齿差异(通常可忽略)
- 变暗的原始图像 = 匹配的区域(以 30% 不透明度显示以提供上下文)
- 大的红色集群 = 结构性问题(缺失元素、布局移位、尺寸错误)
- 分散的红色点 = 子像素渲染、字体平滑或抗锯齿(通常可接受)
- 边缘的红色条带 = 尺寸不匹配或内边距/外边距差异
步骤 6:解释并操作
将定量 JSON 数据与视觉差异检查相结合:
| diffPercent | 解释 | 操作 |
|---|---|---|
| < 1% | 视觉上相同 | 仅存在微小的子像素差异。无需操作。 |
| 1–5% | 接近匹配 | 检查差异图像中的间距、字体粗细或边框差异。可能需要小的 CSS 调整。 |
| 5–15% | 显著差异 | 布局移位、颜色不匹配或缺失元素。审查差异中的红色集群以确定哪些部分需要处理。 |
| > 15% | 重大差距 | 主要的结构或样式差异。首先关注最大的红色区域——这些指示最大的布局差异。 |
解读差异图像时,请注意:
- 页面上的红色集群位于何处?(顶部、中间、底部、左侧、右侧)
- 它们是否与特定元素相关联?(导航、英雄区域、卡片、页脚)
- 集群是否暗示间距问题、颜色差异或内容缺失?
步骤 7:修复并重新差异
- 根据差异分析进行 CSS/HTML 修正
- 重复步骤 3–6(仅重新截取当前 URL 的截图)
- 持续进行,直到
diffPercent达到可接受的水平
典型目标:
- 精确复制:< 2%
- 合理匹配:< 5%
- 结构匹配(内容不同):< 15%
整页截图
默认情况下,截图仅捕获可见视口。对于长页面,使用 fullPage 选项:
mcp_playwright_browser_take_screenshot → {
type: "png",
fullPage: true,
filename: "temp/vdiff-baseline-desktop-full.png"
}
警告: 整页截图会产生大图像并导致更慢的差异计算。仅在比较整个页面布局时使用。
Storybook 专属提示
截取 Storybook 故事的截图时:
-
使用 iframe URL 以获得更干净的截图(无 Storybook chrome):
http://localhost:6006/iframe.html?id=components-card--default&viewMode=story -
等待渲染——Storybook 故事可能需要一些时间才能完成水合:
mcp_playwright_browser_wait_for → { time: 3 } -
元素截图非常适合将故事内容与任何 Storybook 内边距隔离开来。
阈值调整
--threshold 标志(0 到 1)控制 pixelmatch 灵敏度:
| 值 | 灵敏度 | 使用场景 |
|---|---|---|
| 0.05 | 非常严格 | 精确像素匹配,捕获所有内容 |
| 0.1 | 默认 | 良好的平衡,忽略大多数抗锯齿 |
| 0.2 | 宽松 | 容忍跨系统的字体渲染差异 |
| 0.3 | 非常宽松 | 仅捕获主要的颜色/布局差异 |
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-desktop.png \
--current temp/vdiff-current-desktop.png \
--output temp/vdiff-diff-desktop.png \
--threshold 0.2
示例:比较英雄区域
# 1. 截取基准截图(生产站点英雄区域)
mcp_playwright_browser_navigate → { url: "https://www.bitovi.com/" }
mcp_playwright_browser_resize → { width: 1280, height: 720 }
mcp_playwright_browser_wait_for → { time: 2 }
mcp_playwright_browser_snapshot → {}
# 找到英雄区域的引用,例如 ref="hero-[1]"
mcp_playwright_browser_take_screenshot → {
type: "png",
element: "hero section",
ref: "hero-[1]",
filename: "temp/vdiff-baseline-hero-desktop.png"
}
# 2. 截取当前截图(Storybook 故事)
mcp_playwright_browser_navigate → { url: "http://localhost:6006/iframe.html?id=components-hero--default" }
mcp_playwright_browser_resize → { width: 1280, height: 720 }
mcp_playwright_browser_wait_for → { time: 3 }
mcp_playwright_browser_take_screenshot → {
type: "png",
filename: "temp/vdiff-current-hero-desktop.png"
}
# 3. 运行差异(在终端中)
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-hero-desktop.png \
--current temp/vdiff-current-hero-desktop.png \
--output temp/vdiff-diff-hero-desktop.png
# 4. 启动临时服务器(每个会话一次,在后台终端中)
npx -y http-server temp/ -p 8787 --cors -c-1 &
# 5. 通过 Playwright 查看差异图像
mcp_playwright_browser_navigate → { url: "http://localhost:8787/vdiff-diff-hero-desktop.png" }
mcp_playwright_browser_take_screenshot → {} # 触发视觉
# 6. 解释:读取 JSON 输出 + 视觉检查差异
# 7. 修复样式,重新截取当前截图,重新差异
清理
所有输出文件都进入 temp/,该目录已被 git 忽略。要在会话后清理:
rm -f temp/vdiff-*.png