前端设计走查平台实践(后端篇)

简介: 设计师在进行走查的过程中,肉眼的比对偶尔会忽略一些细微部分,同时也会耗费设计师大量的精力,为了辅助设计同学能够更高效的进行设计走查,本文旨在通过设计走查平台在后端侧的实践总结下对于视觉稿还原程度比对的一些思路。

后端 | 前端设计走查平台实践(后端篇).png

项目背景

随着业务的不断发展,研发链路的效能提升也是一个至关重要的指标,其中对前端工程基建而言,其上游部分主要是和设计师同学打交道,而在整个研发链路中,通常会有设计走查的流程来让设计师同学辅助测试同学完成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

用于图片的上传,使用 multermultipart/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领域,对于前端智能化方向感兴趣的同学,可以着重在这个角度多多研讨,共勉!!!

参考

相关文章
|
8天前
|
消息中间件 API 数据库
构建微服务架构的后端实践
【7月更文挑战第7天】本文将深入探讨微服务架构在后端开发中的应用,从微服务的理论基础出发,逐步引导读者了解如何在实际项目中设计、部署和维护一套高效的微服务系统。我们将通过一个虚构的电商平台案例,展示微服务架构的搭建过程,包括服务拆分、数据库设计、通信机制选择、容错与服务治理等关键步骤,旨在为后端开发者提供一份实战指南。
83 4
|
8天前
|
前端开发 JavaScript API
现代前端开发中的Web组件化设计与实践
在现代前端开发中,Web组件化已经成为了一个关键的设计思想和实践方法。本文探讨了Web组件化的概念、优势以及如何在实际项目中进行设计和应用。通过分析实例和最佳实践,展示了如何利用组件化开发提升前端开发效率和代码可维护性,同时也解决了在大型项目中常见的代码重用和团队协作问题。
|
8天前
|
编解码 前端开发 UED
现代前端开发中的响应式设计与实践
响应式设计已经成为现代前端开发的必备技能。本文探讨了响应式设计的基本概念、重要性以及实际应用中的最佳实践,帮助开发者在不同设备上提供一致且优雅的用户体验。
|
10天前
|
前端开发 NoSQL 数据库
部署常用的流程,可以用后端,连接宝塔,将IP地址修改好,本地只要连接好了,在本地上前后端跑起来,前端能够跑起来,改好了config.js资料,后端修改好数据库和连接redis,本地上跑成功了,再改
部署常用的流程,可以用后端,连接宝塔,将IP地址修改好,本地只要连接好了,在本地上前后端跑起来,前端能够跑起来,改好了config.js资料,后端修改好数据库和连接redis,本地上跑成功了,再改
|
1天前
|
前端开发 JavaScript API
告别‘老司机’时代,AJAX与Fetch API让你的前端与Python后端无缝对接!
【7月更文挑战第14天】前端与后端交互的关键技术是AJAX和Fetch API。AJAX允许不刷新页面更新内容,而Fetch API提供了Promise基
|
1天前
|
编解码 前端开发 开发者
现代前端开发中的响应式设计原则与实践
在现代前端开发中,响应式设计不再是可选的额外功能,而是确保用户体验和网站可访问性的核心要素。本文将探讨响应式设计的基本原则、实施技术以及最佳实践,帮助开发者有效地构建适应不同设备和分辨率的用户界面。
|
1天前
|
编解码 缓存 前端开发
现代前端开发中的响应式设计实践与优化
在当今快节奏的互联网环境中,用户对网页的访问设备多样化,响应式设计成为前端开发中不可或缺的一环。本文探讨了响应式设计的重要性以及实现中的最佳实践,涵盖了基本概念、布局策略、以及性能优化的关键技术,为开发人员提供了全面的指导与思路。
|
4天前
|
前端开发
后端一次返回大量数据,前端做分页处理
后端一次返回大量数据,前端做分页处理
6 0
|
7天前
|
前端开发 JavaScript Java
开发做前端好还是后端好?
开发做前端好还是后端好?
|
9天前
|
前端开发
若依部署,部署常见流程之先部署网页的后端系统,让自己的前端能够看到内容,先部署后端,让前端在本地跑起来-----吃饱了撑死了大佬建议,正确的部署流程
若依部署,部署常见流程之先部署网页的后端系统,让自己的前端能够看到内容,先部署后端,让前端在本地跑起来-----吃饱了撑死了大佬建议,正确的部署流程