基于CasperJS和resemble.js的像素级UI对比服务实现

作者:南风

时间:2026-10-22

浏览:463

本文详细介绍如何利用Node.js构建一个像素级UI对比服务。通过CasperJS模拟浏览器截图获取前端页面,结合resemble.js进行图像像素差异分析,自动生成包含差异度参数的对比报告。文章涵盖服务架构、核心代码实现、环境变量配置及部署步骤,旨在为前端开发与测试人员提供自动化视觉回归测试的解决方案。

文章目录

引言与效果预览

前言

本文分享一个基于Node.js构建的服务,用于将设计稿与前端页面进行像素级对比,旨在辅助测试人员或前端开发者完成视觉回归测试。在像素级别的严格比对下,网页对设计稿的还原度差异将直观呈现。以下是具体实现细节。

效果预览

引言与效果预览

前置知识与核心工具

前置知识

本服务主要依赖以下两个库作为核心工具:

  • CasperJS:基于PhantomJS开发,内部封装了无界面浏览器。它允许通过代码模拟用户操作浏览器,支持鼠标事件等多种功能,本文主要利用其截图能力。
  • resemble.js:用于图片像素对比的工具。其基本用法是传入两张图片,返回一张合成图及差异度等参数。实现原理是将图片转换为Canvas后获取像素点,逐一进行比对。

基于上述工具,服务的大致思路为:利用CasperJS访问指定网站并截取页面,随后将截取的图片与设计稿进行比对,最终输出比对结果。

服务整体架构与流程

整体思路

引言与效果预览

根据流程图,整体处理逻辑如下:

  • 从前端接收设计稿图片、目标网站地址及需要截取的节点信息。
  • 将接收到的设计稿图片保存至images文件夹。
  • 启动子进程运行CasperJS,对目标网站指定页面进行截图。
  • 截图完成后,请求form.html页面,将图片地址信息填入表单并提交回服务器。
  • 服务端获取提交图片后,通过resemble.js将截图与设计稿进行像素比对。
  • 将比对结果返回至前端页面。

此处可能存在的疑问是:为何在CasperJS中完成截图后,不直接传回服务器,而是选择通过打开表单页面提交信息?

原因在于CasperJS运行于操作系统层面,并非Node.js模块,目前尚未发现CasperJS与Node服务之间直接通信的有效方法。因此,采取折中方案:通过CasperJS快速打开预先编写的表单页面,填写图片信息后提交至服务器,以此完成数据回传。

核心功能实现细节

实现细节

实现一个简易静态服务器

由于需要返回index.html和form.html页面,需实现一个简易静态文件服务器。代码示例如下:


const MIME_TYPE = {
 "css": "text/css",
 "gif": "image/gif",
 "html": "text/html",
 "ico": "image/x-icon",
 "jpeg": "image/jpeg",
 "jpg": "image/jpg",
 "js": "text/javascript",
 "json": "application/json",
 "pdf": "application/pdf",
 "png": "image/png",
 "svg": "image/svg+xml",
 "swf": "application/x-shockwave-flash",
 "tiff": "image/tiff",
 "txt": "text/plain",
 "wav": "audio/x-wav",
 "wma": "audio/x-ms-wma",
 "wmv": "video/x-ms-wmv",
 "xml": "text/xml"
}
function sendFile(filePath, res) {
 fs.open(filePath, 'r+', function(err){ //根据路径打开文件
  if(err){
   send404(res)
  }else{
   let ext = path.extname(filePath)
   ext = ext ? ext.slice(1) : 'unknown'
   let contentType = MIME_TYPE[ext] || "text/plain" //匹配文件类型
   fs.readFile(filePath,function(err,data){
    if(err){
     send500(res)
    }else{
     res.writeHead(200,{'content-type':contentType})
     res.end(data)
    }
   })
  }
 })
}

解析表单并将图片存储到images文件夹


const multiparty = require('multiparty') //解析表单
let form = new multiparty.Form()
 form.parse(req, function (err, fields, files) {
  let filename = files['file'][0].originalFilename,
   targetPath = __dirname + '/images/' + filename,
  if(filename){
   fs.createReadStream(files['file'][0].path).pipe(fs.createWriteStream(targetPath))
   ...
  } 
 })

通过创建可读流读取文件内容,再利用pipe管道写入指定路径,即可保存上传的图片。

运行CasperJS


const { spawn } = require('child_process')
spawn('casperjs', ['casper.js', filename, captureUrl, selector, id])
casperjs.stdout.on('data', (data) => {
 ...
}) 

可通过spawn方法创建子进程启动CasperJS,也可使用exec等命令。

截图并提交数据到form.html


const system = require('system')
const host = 'http://10.2.45.110:3033'
const casper = require('casper').create({
 // 浏览器窗口大小
 viewportSize: {
  width: 1920,
  height: 4080
 }
})
const fileName = decodeURIComponent(system.args[4])
const url = decodeURIComponent(system.args[5])
const selector = decodeURIComponent(system.args[6])
const id = decodeURIComponent(system.args[7])
const time = new Date().getTime()
casper.start(url)
casper.then(function() {
  console.log('正在截图请稍后')
  this.captureSelector('./images/casper'+ id + time +'.png', selector)
})
casper.then(function() {
 casper.start(host + '/form.html', function() {
  this.fill('form#contact-form', {
   'diff': './images/casper'+ id + time +'.png',
   'point': './images/' + fileName,
   'id': id
  }, true)
 })
})
casper.run()

代码逻辑相对简单,主要过程为打开页面,在then回调中定义操作,最后执行run。由于不清楚如何与Node服务直接通信,此处选择开启新页面提交数据。深入研究者可参考CasperJS官方文档。

通过resemble.js进行像素比对并返回数据


function complete(data) {
  let imgName = 'diff'+ new Date().getTime() +'.png',
   imgUrl,
   analysisTime = data.analysisTime,
   misMatchPercentage = data.misMatchPercentage,
   resultUrl = './images/' + imgName
  fs.writeFileSync(resultUrl, data.getBuffer())
  imgObj = {
   ...
  }
  let resEnd = resObj[id] // 找回最开始的res返回给页面数据
  resEnd.writeHead(200, {'Content-type':'application/json'})
  resEnd.end(JSON.stringify(imgObj))
 }
let result = resemble(diff).compareTo(point).ignoreColors().onComplete(complete)

此处涉及一个关键问题:如何将最终结果返回给最初的请求。由于经过多次中转,原始的响应对象res已难以直接获取。经过多次尝试,暂时采用设定全局对象的方法:在接收初始请求时,将请求者的IP和时间戳设为唯一ID,作为全局对象的key,value为当前的res对象。在整个中转流程中传递该ID,最后通过调用resObj[id]获取原始响应对象并返回数据。此方法虽非最优,但能确保服务跑通。若有更优方案,欢迎交流。

环境部署与配置

部署

安装PhantomJS(macOS环境)

从官网下载:phantomjs-2.1.1-macosx.zip

解压路径:/User/xxx/phantomjs-2.1.1-macosx

添加环境变量:在~/.bash_profile文件中添加配置。

export PATH="$PATH:/Users/xxx/phantomjs-2.1.1-macosx/bin"

在terminal中输入:phantomjs --version

若能看到版本号,即表示安装成功。

安装CasperJS


brew update && brew install casperjs

安装resemble.js


cnpm i resemblejs //已写进packjson可不用安装
brew install pkg-config cairo libpng jpeg giflib
cnpm i canvas //node内运行canvas

Node服务启动


git clone https://github.com/Aaaaaaaty/gui-auto-test.git
cd gui-auto-test
cnpm i
cd pxdiff
nodemon server.js

打开http://localhost:3033/index.html

惯例附上作者博客,不定时更新中。

参考文献

  • PhantomJS 安装指南
  • CasperJS 文档
  • resemble.js 文档

总结与参考

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章