本文详细介绍如何利用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 文档
总结与参考
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。