Communitygithub.com

视觉差异对比

通过 Playwright 在多个断点截取基线 URL 与开发或 Storybook URL 的截图,进行像素级图像差异对比,并报告结果以指导样式和 HTML 修正。适用于复制现有页面或组件、验证视觉准确性或检查响应式保真度。

视觉差异对比 是什么?

此技能提供了一种系统化的方法,通过 Playwright 的自动化截图和像素级图像差异检测,比较两个 URL(例如,生产站点与本地开发服务器或 Storybook 故事)的视觉呈现。它旨在帮助开发人员在复制现有页面或验证组件视觉准确性时,精确定位布局、间距和样式上的差异。该技能支持移动端、平板和桌面等多个视口断点,并提供了针对特定组件的裁剪策略(如使用 CSS 选择器),以确保差异分析不受页面其他元素干扰。它通过 JSON 输出差异百分比,并生成高亮差异(红色像素表示差异,黄色像素表示可忽略的抗锯齿差异)的差异图像,供用户视觉检查。该技能适用于 Claude Code、Cursor 和 Codex 等 AI 代理平台,通过 Playwright MCP 服务器与浏览器交互,并在终端中运行一个 Node.js 脚本来执行图像差异比较。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/bitovi/ai-enablement-prompts/tree/HEAD/plugins/playwright/skills/visual-diff

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

技能:视觉差异对比

比较两个 URL 的视觉呈现——基准(如生产站点)和当前(如本地开发服务器或 Storybook 故事)——以发现像素级差异并指导修正。


何时使用

  • 在 Storybook 或 Astro 站点中复制现有页面或部分
  • 验证组件相对于参考设计的视觉准确性
  • 样式更改后检查跨断点的响应式保真度
  • 迭代 CSS/HTML 直到组件与其参考匹配

何时不使用

  • 仅内容更改(文本更新、文字编辑),布局不变
  • 比较来自 Figma 的设计——改为使用 Figma MCP 技能
  • 性能或可访问性审计——使用 Lighthouse 或 a11y 插件

前提条件

开始前,请确认:

  1. Playwright MCP 已在 .vscode/mcp.json 中配置,带有 --caps=vision
  2. pixelmatch 和 pngjs 已安装:检查 package.json 的 devDependencies
  3. 基准 URL 可访问(外部站点或正在运行的服务器)
  4. 当前 URL 可访问(:4321 上的开发服务器、:6006 上的 Storybook 等)
  5. 工作区根目录中存在 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 故事仅渲染该组件,但基准页面包含其周围的完整页面时——裁剪到组件的边界框,以便差异不会被不相关的页面部分(导航栏、粘性覆盖层、上下方的其他模块)污染。

如何在基准页面上裁剪:

  1. 导航到基准 URL
  2. 将视口大小调整为 { width: 1280, height: 720 }
  3. 将目标元素滚动到视图中:
    () => document.querySelector('<selector>').scrollIntoView({ behavior: 'instant', block: 'start' })
    
  4. 测量组件的渲染高度:
    () => document.querySelector('<selector>').getBoundingClientRect().height
    
  5. 将视口高度调整为该确切高度(保持宽度不变):
    mcp_playwright_browser_resize → { width: 1280, height: <measured height> }
    
  6. 截图——现在只捕获组件区域。

背景溢出警告: 组件背后可见的内容在 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"
   }

针对每个断点重复:

断点宽度高度文件名后缀
移动端375667-mobile
平板7681024-tablet
桌面1280720-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:修复并重新差异

  1. 根据差异分析进行 CSS/HTML 修正
  2. 重复步骤 3–6(仅重新截取当前 URL 的截图)
  3. 持续进行,直到 diffPercent 达到可接受的水平

典型目标:

  • 精确复制:< 2%
  • 合理匹配:< 5%
  • 结构匹配(内容不同):< 15%

整页截图

默认情况下,截图仅捕获可见视口。对于长页面,使用 fullPage 选项:

mcp_playwright_browser_take_screenshot → {
  type: "png",
  fullPage: true,
  filename: "temp/vdiff-baseline-desktop-full.png"
}

警告: 整页截图会产生大图像并导致更慢的差异计算。仅在比较整个页面布局时使用。


Storybook 专属提示

截取 Storybook 故事的截图时:

  1. 使用 iframe URL 以获得更干净的截图(无 Storybook chrome):

    http://localhost:6006/iframe.html?id=components-card--default&viewMode=story
    
  2. 等待渲染——Storybook 故事可能需要一些时间才能完成水合:

    mcp_playwright_browser_wait_for → { time: 3 }
    
  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

相关技能