我的项目背景
随着业务的一直倒退,研发链路的效力晋升也是一个至关重要的指标,其中对前端工程基建而言,其上游局部次要是和设计师同学打交道,而在整个研发链路中,通常会有设计走查的流程来让设计师同学辅助测试同学实现 UI 测试。设计师在进行走查的过程中,肉眼的比对偶然会疏忽一些轻微局部,同时也会消耗设计师大量的精力,为了辅助设计同学可能更高效的进行设计走查,本文旨在通过设计走查平台在后端侧的实际总结下对于视觉稿还原水平比对的一些思路。
计划
后端架构选型,对于前端基建局部的后端利用而言,通常是抉择 node.js 来进行解决,尽管走查平台后端波及到了图片的比照计算,然而在团体层面提供了各种云服务,因此能够利用云服务相干的各种中间件来保障前端基建后端服务的高可用与高性能。设计走查平台波及到了图片上传后的长期存储,如果应用云存储,比方:对象存储等,势必波及到大量的与云平台的交互,较为繁琐,而本利用业务次要是用于解决两张图片的比对,计算要求要高于存储要求,因此抉择临时文件存储在本地零碎中,但这就带来了一个问题,那就是大量比照需要可能会将服务搞崩,思考到 node.js 服务的多过程单线程的个性,咱们这里引入了 pm2 来进行过程治理,同时应用定时工作 cron 对临时文件进行定时清理(ps:这里也能够利用 k8s 的定时工作进行解决),保障业务的可用性。
目录
-
db
- temp
-
server
-
routes
-
piper
- compare
- upload
- index.js
-
- app.js
-
- ecosystem.config.js
实际
对图片比对局部,这里应用 looks-same
库来进行 png 图片的比对,其本质是通过 (x,y) 像素的差别比对进行 pixel 图片的笼罩描述,最初输入一个比照叠加的图片,其余的库还有 pixel-match
以及 image-diff
等都能够来进行图片的比对
源码
piper
upload
用于图片的上传,应用 multer
将 multipart/form-data
进行转换
const router = require('../../router');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const storage = multer.diskStorage({destination: function(req, file, cb) {if(file.mimetype == 'image/png') {cb(null, path.resolve(__dirname, '../../../../db/__temp__'))
} else {cb({ error: 'Mime type not supported'})
}
},
filename: function(req, file, cb) {cb(null, `${Date.now()}.${file.originalname}`)
}
})
/**
* @openapi
* /piper/upload/putImage:
post:
summary: 上传图片
tags:
- putImage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/putImage'
responses:
'200':
content:
application/json:
example:
code: "0"
data: ""msg:" 胜利 "
success: true
*/
router.post('/putImage', multer({storage: storage}).single('img'), async function (req, res) {console.log('putImage', req.file);
// 定时删除上传的图片
setTimeout(() => {if(fs.existsSync(path.resolve(__dirname, `../../../../db/__temp__/${req.file.filename}`))) {fs.unlink(path.resolve(__dirname, `../../../../db/__temp__/${req.file.filename}`), function (err) {if (err) {console.error(` 删除文件 ${req.file.filename} 失败,失败起因:${err}`)
}
console.log(` 删除文件 ${req.file.filename} 胜利 `)
});
} else {console.log(` 文件 ${req.file.filename} 不存在 `)
}
}, 120 * 1000)
return res.json({
code: "0",
data: {
filename: req.file.filename,
size: req.file.size
},
msg: '胜利',
success: true
})
});
module.exports = router;
compare
应用 looks-same
对图片进行比对,点击下载后能够获取比照的图片
const router = require('../../router');
const looksSame = require('looks-same');
const path = require('path');
const fs = require('fs');
/**
* @openapi
* /piper/compare/compareImage:
post:
summary: 比拟图片
tags:
- compareImage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/compareImage'
responses:
'200':
content:
application/json:
example:
code: "0"
data: ""msg:" 胜利 "
success: true
*/
router.post('/compareImage', function (req, res) {console.log('compareImage', req.body);
const {
designName,
codeName
} = req.body;
if (designName && codeName) {if (fs.existsSync(path.resolve(__dirname, `../../../../db/__temp__/${designName}`)) && fs.existsSync(path.resolve(__dirname, `../../../../db/__temp__/${codeName}`))) {const [, ...d] = designName.split('.'),
[, ...c] = codeName.split('.');
d.pop();
c.pop();
const dName = d.join(''),
cName = c.join('');
const compareName = `${Date.now()}.${dName}.${cName}.png`;
looksSame.createDiff({reference: path.resolve(__dirname, `../../../../db/__temp__/${designName}`),
current: path.resolve(__dirname, `../../../../db/__temp__/${codeName}`),
diff: path.resolve(__dirname, `../../../../db/__temp__/${compareName}`),
highlightColor: '#ff00ff', // color to highlight the differences
strict: false, // strict comparsion
tolerance: 2.5,
antialiasingTolerance: 0,
ignoreAntialiasing: true, // ignore antialising by default
ignoreCaret: true // ignore caret by default
}, function (error) {if (error) {
return res.json({
code: "-1",
data: error,
msg: '失败',
success: false
})
} else {[codeName, designName].forEach(item => {fs.unlink(path.resolve(__dirname, `../../../../db/__temp__/${item}`), function (err) {if (err) {console.error(` 删除文件 ${item} 失败,失败起因:${err}`)
}
console.log(` 删除文件 ${item} 胜利 `)
});
})
return res.json({
code: "0",
data: {compareName: compareName},
msg: '胜利',
success: true
})
}
});
} else {
return res.json({
code: "-1",
data: '所需比照图片不存在,请从新确认',
msg: '失败',
success: false
})
}
} else {
return res.json({
code: "-1",
data: '所需比照图片无奈找到,请从新确认',
msg: '失败',
success: false
})
}
});
/**
* @openapi
* /piper/compare/downloadImage:
post:
summary: 下载比拟图片
tags:
- downloadImage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/downloadImage'
responses:
'200':
content:
application/json:
example:
code: "0"
data: ""msg:" 胜利 "
success: true
*/
router.post('/downloadImage', function (req, res) {console.log('downloadImage', req.body);
const {compareName} = req.body;
if (compareName) {const f = fs.createReadStream(path.resolve(__dirname, `../../../../db/__temp__/${compareName}`));
res.writeHead(200, {
'Content-Type': 'application/force-download',
'Content-Disposition': 'attachment; filename=' + compareName
});
f.on('data', (data) => {res.write(data);
}).on('end', () => {res.end();
fs.unlink(path.resolve(__dirname, `../../../../db/__temp__/${compareName}`), function (err) {if (err) {console.error(` 删除文件 ${compareName} 失败,失败起因:${err}`)
}
console.log(` 删除文件 ${compareName} 胜利 `)
});
})
} else {
return res.json({
code: "-1",
data: '生成比照图片名称不正确,请从新确认',
msg: '失败',
success: false
})
}
});
module.exports = router;
app.js
定时工作,每天 23 点 59 分定时清理临时文件
// 每天 23 点 59 分删除临时文件
cron.schedule("59 23 * * *", function() {console.log("---------------------");
console.log("Cron Job Start");
const files = fs.readdirSync(path.resolve(__dirname, '../db/__temp__'));
if(files.length > 0) {files.forEach(file => fs.unlinkSync(path.resolve(__dirname, `../db/__temp__/${file}`)));
}
console.log("Cron Job Done");
console.log("---------------------");
});
ecosystem.config.js
pm2
相干的一些配置,这里开启了 3 个实例进行监听
module.exports = {
apps: [
{
name: 'server',
script: './server',
exec_mode: 'cluster',
instances: 3,
max_restarts: 4,
min_uptime: 5000,
max_memory_restart: '1G'
}
]
}
总结
前端设计走查平台的后端接口局部外围在于图片的比对,而对于图片类似度的比拟,通常又会波及到图像处理相干的内容,这里应用的是 looks-same
这个开源库,其本质利用像素之间的匹配来计算类似度,另外还有利用余弦类似度、哈希算法、直方图、SSIM、互信息等,除了这些传统办法外,还能够应用深度学习的办法来解决,常见的有如特征值提取 + 特征向量类似度计算的办法等等,这就波及到了前端智能化的畛域,对于这部分感兴趣的同学能够参看一下蚂蚁金服的蒙娜丽莎这个智能化的设计走查平台的实现(ps:更智能的视觉验收提效计划 – 申姜)。在前端智能化畛域中,通常利用场景都是与上游设计局部的落地中,比方 D2C 和 C2D 畛域,对于前端智能化方向感兴趣的同学,能够着重在这个角度多多研究,共勉!!!
参考
- 你晓得 pm2 是怎么工作的吗
- Node 的 Cluster 模块和 PM2 的原理介绍
- Linux 惊群效应之 Nginx 解决方案
- 设计小姐姐都说好的视觉还原比照利器
- 重点:机器学习总结之各算法罕用包和函数
- 图片类似度计算方法总结