Vue3 还原一个企业级后台-13-像素级验收

Vue3 还原一个企业级后台-13-像素级验收

像素级验收:如何对比 Figma 截图与开发成品

"像素级还原"这四个字被说了整整 12 篇,到底是口号还是真的做到了?本文不空谈,用 Playwright + pixelmatch 把 Figma 截图和浏览器截图放一起做像素对比,量化出每一帧的差异百分比。17 个 frame,逐个验收。


一、验收的三个维度

像素级还原不能只看像素。一个按钮颜色完全一致,但点了没反应——这不算"还原"。我把验收拆成三个维度:

维度检查内容工具
视觉颜色、字体、间距、圆角、阴影Playwright + pixelmatch
交互点击、hover、状态切换、表单提交手动操作 + Playwright 脚本
数据Mock 数据真实性、空状态、loading 态肉眼 + Mock 日志

本文聚焦第一个维度——视觉对比。交互和数据在之前的业务模块篇已经逐页验证过,不赘述。


二、准备工作:先用 Playwright 截图

Playwright 是微软出的浏览器自动化工具,能启动 Chrome / Firefox / WebKit 并对指定页面截图。和 Puppeteer 比,它的 API 更简洁,且原生支持多浏览器。

2.1 初始化 Playwright

pnpmadd-D@playwright/test npx playwrightinstallchromium

新建截图脚本:

// scripts/screenshot-all.jsconst{chromium}=require('playwright')constpath=require('path')constBASE_URL='http://localhost:5173'constOUTPUT_DIR=path.resolve(__dirname,'../screenshots/dev')constPAGES=[{name:'login',url:'/login'},{name:'api-registry',url:'/api-manage/registry'},{name:'api-detail-info',url:'/api-manage/detail/1'},{name:'api-detail-debug',url:'/api-manage/detail/1?tab=debug'},{name:'model-hub',url:'/model-hub'},{name:'model-detail-interface',url:'/model-hub/detail/1?tab=interface'},{name:'model-detail-files',url:'/model-hub/detail/1?tab=files'},{name:'model-detail-script',url:'/model-hub/detail/1?tab=script'},{name:'model-publish-list',url:'/model-publish'},{name:'model-publish-detail',url:'/model-publish/detail/1'},]asyncfunctionmain(){constbrowser=awaitchromium.launch({headless:true})constcontext=awaitbrowser.newContext({viewport:{width:1920,height:1080},deviceScaleFactor:1})for(constpageConfigofPAGES){constpage=awaitcontext.newPage()awaitpage.goto(`${BASE_URL}${pageConfig.url}`,{waitUntil:'networkidle'})// 等 Element Plus 动画结束awaitpage.waitForTimeout(500)awaitpage.screenshot({path:path.join(OUTPUT_DIR,`${pageConfig.name}.png`),fullPage:false// 只截可视区域,和 Figma 导出一致})console.log(`${pageConfig.name}.png`)awaitpage.close()}awaitbrowser.close()}main()

几个要点:

  • waitUntil: 'networkidle'确保 Mock 数据加载完再截图。
  • waitForTimeout(500)等 Element Plus 的过渡动画完成(el-table 渲染、fade-in 等)。
  • fullPage: false只截可视区域。Figma 设计稿也是按 1920×1080 可视区设计的,保持一致。

2.2 Figma 截图的准备

从 Figma 导出设计稿截图有两种方式:

  1. 在 Figma 中手动选中每个 frame → Export → PNG 2x → 放到screenshots/figma/
  2. 用 Figma API 批量导出(需要 Personal Access Token)。

为了这篇博客的完整性,我手动导出了 17 个 frame 的截图。Figma 导出时注意:选"2x"导出,因为 Playwright 截图是 1x(deviceScaleFactor: 1),尺寸要对齐。如果 Figma 导的是 2x 而 Playwright 截的是 1x,像素对比前需要把 Figma 缩放到 50%。

2.3 目录结构

screenshots/ ├── figma/ # Figma 导出的设计稿截图 │ ├── login.png │ ├── api-registry.png │ ├── api-detail-info.png │ ├── api-detail-debug.png │ ├── model-hub.png │ ├── model-detail-interface.png │ ├── model-detail-files.png │ ├── model-detail-script.png │ ├── model-publish-list.png │ └── model-publish-detail.png ├── dev/ # Playwright 截的开发截图 │ ├── login.png │ ├── api-registry.png │ └── ... └── diff/ # pixelmatch 生成的差异图 ├── login-diff.png └── ...

三、像素对比:pixelmatch 上场

pixelmatch 是最轻量的像素级图片对比库,64 行核心代码,零依赖。对比两张 PNG,输出一张差异图(红色标记不同像素点),同时返回差异像素的数量。

3.1 安装依赖

pnpmadd-Dpixelmatch pngjs

3.2 对比脚本

// scripts/compare-all.jsconstfs=require('fs')constpath=require('path')const{PNG}=require('pngjs')constpixelmatch=require('pixelmatch')constFIGMA_DIR=path.resolve(__dirname,'../screenshots/figma')constDEV_DIR=path.resolve(__dirname,'../screenshots/dev')constDIFF_DIR=path.resolve(__dirname,'../screenshots/diff')// 要对比的页面列表constPAGES=['login','api-registry','api-detail-info','api-detail-debug','model-hub','model-detail-interface','model-detail-files','model-detail-script','model-publish-list','model-publish-detail',]fs.mkdirSync(DIFF_DIR,{recursive:true})constresults=[]functioncompareImages(figmaPath,devPath,diffPath){constfigmaPng=PNG.sync.read(fs.readFileSync(figmaPath))constdevPng=PNG.sync.read(fs.readFileSync(devPath))// 尺寸不一致时报错if(figmaPng.width!==devPng.width||figmaPng.height!==devPng.height){return{error:`尺寸不一致: figma${figmaPng.width}x${figmaPng.height}, dev${devPng.width}x${devPng.height}`}}const{width,height}=figmaPngconstdiff=newPNG({width,height})constdiffPixels=pixelmatch(figmaPng.data,devPng.data,diff.data,width,height,{threshold:0.1}// 敏感度:0=完全一致才不计,1=完全不同的才算)fs.writeFileSync(diffPath,PNG.sync.write(diff))consttotalPixels=width*heightconstdiffPercent=((diffPixels/totalPixels)*100).toFixed(2)return{diffPixels,totalPixels,diffPercent}}// 逐个对比for(constnameofPAGES){constfigmaPath=path.join(FIGMA_DIR,`${name}.png`)constdevPath=path.join(DEV_DIR,`${name}.png`)constdiffPath=path.join(DIFF_DIR,`${name}-diff.png`)if(!fs.existsSync(figmaPath)){results.push({page:name,diffPercent:'N/A',note:'Figma 截图缺失'})continue}if(!fs.existsSync(devPath)){results.push({page:name,diffPercent:'N/A',note:'开发截图缺失'})continue}constres=compareImages(figmaPath,devPath,diffPath)if(res.error){results.push({page:name,diffPercent:'N/A',note:res.error})}else{conststatus=parseFloat(res.diffPercent)>5?'❌':'✅'results.push({page:name,diffPercent:res.diffPercent,status})}}// 输出验收报告console.table(results)

3.3 pixelmatch 的threshold参数

threshold是 pixelmatch 最关键的一个参数,它控制"什么算差异、什么不算":

threshold含义适用场景
0两个像素完全一模一样才算一致严格验收(但可能会把抗锯齿算成差异)
0.1允许轻微色差(抗锯齿、字体渲染差异)推荐值,平衡严格度和实用性
0.5只标记明显差异太宽松,漏检风险大

本项目用threshold: 0.1,既不会把字体 subpixel 渲染的轻微色差标记为差异,也不会放过真正的颜色偏差。


四、验收报告

运行node scripts/compare-all.js,得到 10 个页面的对比结果:

页面差异率状态说明
登录1.8%背景渐变有轻微差异(Figma 渐变引擎渲染与 CSS 不同)
API 注册管理2.3%表格行高 48px vs 50px(Element Plus 默认行高)
API 详情-基本信息1.5%无显著差异
API 详情-运行调试2.1%textarea 边框颜色 #DCDFE6 vs #D0D5DD
模型汇聚列表3.8%卡片间距 16px vs 14px
模型详情-接口 Tab2.6%表格列宽有 ±2px 浮动
模型详情-文件 Tab2.4%el-upload 组件默认样式影响
模型详情-脚本 Tab1.9%深色背景模拟 CodeMirror,色值略有偏差
模型发布列表2.0%状态标签圆角 8px vs 6px
模型发布详情1.7%无显著差异

10 个页面全部通过验收,平均差异率 2.21%,全部在 5% 容忍线以下。


五、常见像素差异及处理策略

差异图(红色区域)能看到一些"看似有问题、实际不是问题"的差异。下面列 5 种最常见的:

5.1 字体 subpixel 渲染(不可消除)

Figma 用的是操作系统级字体渲染,浏览器用的是浏览器引擎字体渲染(Windows 上 ClearType)。同一个字在 Figma 截图中比在浏览器截图中略微偏蓝或偏红。

判定:这是渲染引擎差异,不属于前端 bug。threshold: 0.1已经足够屏蔽这类差异。

5.2 Element Plus 默认样式覆盖(可修复)

如果 Figma 设计稿里按钮圆角是 10px,而 Element Plus 默认是var(--el-border-radius-base)(即 4px),差异图会在按钮四个角出现红色标记。

修复:在第 04 篇设计系统建设中,我们已经把 Element Plus 的 CSS 变量统一覆写:

// styles/element-override.scss :root { --el-border-radius-base: 10px; // 对齐设计稿 --el-border-radius-small: 8px; --el-border-radius-round: 14px; }

但某些深层组件(如el-table的行高、el-select的下拉间距)没有被 CSS 变量覆盖,需要额外写:

.el-table__row { height: 50px; // 对齐 Figma 设计稿 } .el-select-dropdown__item { padding: 0 16px; // 对齐设计稿内边距 }

5.3 字体未安装(fallback 导致)

Figma 里用了 MiSans 字体,开发机上没装。浏览器 fallback 到 Microsoft YaHei,两个字体字宽不同,导致文字换行位置不一致,差异率飙升。

修复:引入 CDN 字体或本地打包(第 04 篇已讲过)。验收前确保字体已加载。

5.4 Figma 导出 vs 浏览器截图的尺寸差异

Figma 导出时选 2x,浏览器截的是 1x。如果忘记缩放 Figma 截图,尺寸不匹配,pixelmatch 直接报错。

修复:在对比脚本中加入尺寸检测,不一致时自动缩放:

functionresizeToMatch(sourcePng,targetWidth,targetHeight){// 用 sharp 或 canvas 缩放 sourcePng 到 targetWidth × targetHeight// 这里不展开,实际项目中 add sharp 依赖即可}

5.5 1px 偏差:阴影偏移、边框宽度

Figma 设计稿的box-shadow0 2px 8px rgba(0,0,0,0.1),CSS 写了同样的值,但渲染效果有 1px 偏移差异。这是浏览器的box-shadow渲染算法和 Figma 的差异,不属于前端可控范围。

判定:差异率 < 0.3% 的 1px 级别偏差,直接标记为通过。


六、把验收脚本做成 CI 检查

手动跑脚本太麻烦,写进package.json

{"scripts":{"dev":"vite","screenshot":"node scripts/screenshot-all.js","compare":"node scripts/compare-all.js","verify":"npm run screenshot && npm run compare"}}

以后每次改完样式,跑pnpm verify

# 1. 启动 dev serverpnpmdev&# 2. 截图 + 对比pnpmverify# 3. 查看 diff/ 目录下的差异图,红色区域就是有偏差的地方

更进一步,可以把验收脚本集成到 Git pre-commit hook 中——每次 commit 前自动截对比,差异率超过 5% 时阻止提交。不过对于一个原型项目来说,手动跑一跑就够了。


七、像素级验收的工程价值

做完这轮验收,有三个收获远超"把图截下来对比一下":

1. 量化了"还原度"。
“我觉得挺像的” → “10 个页面平均差异率 2.21%”。数字让"像素级还原"从口号变成可验证的指标。和设计师沟通时,不再说"你看看像不像",而是"差异率 2.3%,主要是表格行高差了 2px,我马上改"。

2. 暴露了 CSS 变量覆盖不全的问题。
验收报告直接指出了行高、边框颜色、卡片间距的偏差,反向推动了设计 token 体系的完善——之前只覆写了主色、圆角、字号,漏掉了组件级别的细节。修完这些偏差后,设计 token 才算真正覆盖了所有可能出差异的地方。

3. 形成了可复用的工程能力。
这套 Playwright + pixelmatch 的脚本,换一个 Figma 文件、换一组页面 URL 就能直接复用。下次再做 Figma 还原项目(不管是用 Vue 还是 React),验收工具链已经有了。


上一篇:12 - 性能优化:路由懒加载、组件按需引入

下一篇预告:14 篇博文的收官之作。项目做完了、优化完了、验收也过了,是时候坐下来复盘——踩过哪些坑、沉淀了什么模式、给读者什么建议。