肝个斗图机器人,打败隔壁小胖墩

共 12179字,需浏览 25分钟

 ·

2021-11-07 10:13








点击上方 前端Q,关注公众号


回复加群,加入前端Q技术交流群





前言


有一天,组织内的斗图机器人坏掉了,巧不巧的是当你需要用它时,它坏掉了。


赶上要催交同学们的周报,没有表情包,就没办法委婉又不礼仪并友好和善的催促同学们交周报。


然后只能自己做图,打开了度娘,找合适表情,然后打开sketch,一通操作后,粘贴到群,搞定。


but总使用同一表情,又很枯燥,于是又打开度娘,打开sketch,一通操作,粘贴到群,搞定。


过了一段时间,度娘,sketch,群。


又过了一段时间,度娘,sketch,群。


时间一长就会很烦躁,每次都要这样搞半天(难道喜新厌旧属性?感觉像渣男?)


后来,突然开朗。求人不如求己!发挥我的主观能动性!自己敲一个!


于是在经历Node的洗礼,Color的洗礼,Canvas的洗礼,SQL的洗礼,Docker的洗礼,Vercel的洗礼后,它诞生了。



它叫imeme,是一个斗图机器人。



本文目的


给大家介绍如何设计和实现一款斗图机器人,是有前端有后端的全栈开发。


不会讲的





  • 涉及到安全问题、隐私以及制度政策等原因,机器人的接收消息内容不介绍





  • 具体功能演示,不提供截图展示,可自行体验





  • 不会详细讲清楚每一个实现细节




But,这些限制要素无关紧要,不影响全局,也不影响大家搭建自己的机器人。


重点讲解





  • 机器人的技术选型





  • 关键环节的设计思路及相关知识点。




场景还原


使用markdown还原下真实交互场景





image.png


技术选型


明确目标,鼓舞斗志。


那么应该如何设计主体流程?先从最基本的功能入手,列下需求清单:





  • Server,用来接收命令,发送消息。



  • 绘图功能,能够把文字和图片做成一张图。



  • 图片处理,不同的图片类型采取不同策略,获取最基本的图片信息。



  • 数据存储,作为数据源,提供各种有意思的基础图片及与绘图相关的基本参数。



  • 录入导出,便于数据采集,迎支持插入多条数据以及数据库的备份。



  • UI,让imeme用起来更轻松,便于管理数据源,查看图片以及调整绘图参数,还应支持交互式新增和图片下载。


针对如上特点:





  1. Server端,基于express实现node服务,axios + canvas + sql.js。



  2. UI端,vite + vue3.x + typescript设计实现,并提供lib库供多端快捷接入。


整体架构图


简单怼了一张图





image.png


界面管理就很常见了,大致长这样





image.png


关键环节的设计思路



所有源码,链接在文末参考资料中,在github上。服务部署到vercel,可访问体验Web端[3](网速不稳定,毕竟白嫖vercel)



Server


Server要实现,接收到消息命令请求后,绘制图形,并能够给出合理结果反馈,也就是新的图像。所以基于express实现node服务,接口的设计要求如下:





  1. /test 用于测试服务的可用性,get请求。



  2. 设置origin * 允许接口的跨域请求以及多种请求头,默认编码utf-8。



  3. 为Chat端提供的/send,post发送Webhooks消息体。



  4. 为Web端提供的/image/*接口




    • /catalog用于目录获取,读取数据库中存储的图片源列表显示。



    • /open 打开用户选中的列表内容,接口返回图片基本信息(base64及绘图数据)。



    • /save 绘图数据的保存接口,用于图片拖拽编辑后,把最新数据同步到数据库中。



    • /create 新建表情,保存到数据库。



    • /update 更新表情数据



    • /download 下载接口,用户拖拽好的内容,可以直接下载到本地。



    • /export 数据导出备份



Server的接口逻辑在service模块,分为四个层次





  • router.js api接口层,管理服务提供的所有api。



  • data.js 连接接口和数据库的数据层,数据封装,为api提供数据获取服务。



  • ajax.js 请求结果集封装,根据data.js请求,给出结果反馈信息。



  • send.jsChat端提供的发送消息服务


绘图


简单的讲,表情就是图片加文字,即我们常见的水印,选择使用canvas来处理。


Node本身不具备canvas的能力,需要借助`canvas库`[4]来实现基本的绘图能力。


本部分内容在convert模块,主要提供给Chat端使用。



Web端不需要这些,对于浏览器来讲,canvas绘图小菜一碟,属于基本操作。



这里按照功能逻辑设计,分为4个层次:





  • make.js 提供绘图能力,支持图片本地保持。



  • size.js 根据base64串获取图片的widht和height。



  • format.js 菜单格式化,无效命令反馈。



  • parser.js 解析接收到的请求命令。


一个完整的水印图,由很多部分组成,拆解为base64编码的图片,水印文字,文字的位置横纵坐标,文字的颜色,字体大小,对齐方向,最大宽度。


绘图,就是把上述已知信息整合到一起


const make = (text, options) => {
  const base64Img = options.image;
  const parts = base64Img.split(';base64,');
  const type = parts[0].split(':').pop();

  if (NOT_SUPPORT.includes(type) || text === '') {
    return base64Img;
  }

  let base64 = '';
  const {width, height} = getSize(base64Img);

  if (width && height) {
    const img = new Image();
    const canvas = createCanvas(width, height);
    const ctx = canvas.getContext('2d');

    img.onload = () => {
      ctx.drawImage(img, 0, 0);

      const {x, y, font, color, align, max} = options;
      ctx.font = font;
      ctx.fillStyle = color;
      ctx.textAlign = align;
      ctx.fillText(text, x, y, max || width);

      base64 = canvas.toDataURL(type);
    };
    img.onerror = err => {
      console.error(err);
    };
    img.src = base64Img;
  }
  return base64;
};
复制代码

首先,根据base64编码,获取图片内容的基本类型,不同类型的图片,需要不同的解析流程。对于暂不支持水印功能的图片格式或者空命令的请求,直接返回base64原始编码。


接下来,调用size.js中的getSize获取图片的width和height,创建固定大小的canvas画布,进一步,得到ctx。


因为水印中图片在下层,文字在上层,所以先通过ctx.drawImage(img, 0, 0)绘制原始图片,再结合ctx.fillText(text, x, y, max || width)在(x, y)点,绘制最大长度为max的文字信息。


最后,通过base64 = canvas.toDataURL(type)生产出我们需要的绘图后的base64编码。


另外,在make.js中还提供了writeImg方法,可用于在开发中及时本地调试位置参数信息,检测生产的图片是否满足要求。(已经提供UI的交互式调整,解放了本地调试的痛苦)


图片尺寸


这部分内容在size.js,原理是根据base64的buffer,提取image的width和height。


针对不同格式的图片,要采取不同的处理策略,imeme目前提供5种(png/jpg/jpeg/gif/bpm)图片格式的处理,我们以png为例来说明,如何根据图片的buffer获取,真实的尺寸。


这里,你需要一点点的node buffer知识,以及了解简单的图片编码原理。


每种类型的文件都有自己独特的标识,直观上通过文件的扩展名来区分类型,然而扩展名可以随意的更改。所有的文件在计算机上都是以二进制方式存储的,我们可以通过分析标识头来确定文件类型。


我们本地查看任意一个png文件,用十六进制编辑器打开(可使用vscode的hexdump[5]





image.png


我们分析下前两行内容





  • 89 50 4E 47 0D 0A 1A 0A png文件的标识头



  • 00 00 00 0D IHDR头块长度为13 bytes



  • 49 48 44 52 IHDR标识



  • 00 00 00 BC width,换算成十进制为188(16 * 11 + 12)px



  • 00 00 00 C4 height,换算成十进制为196(16 * 12 + 4)px



  • 08 色深,换算下即2^8=256,即256色的图像



  • 06 颜色类型,6表示,带α通道数据的真彩色图像



  • 00 压缩方法,LZ77派生算法(PNG Spec规定此处总为0,非0值为将来使用更好的压缩方法预留)



  • 00 滤波器方法,总为0,同上



  • 00 隔行扫描方法,0表示采用非隔行扫描



  • 25 38 3B 07 4个byte的CRC校验[6]





image.png


在MacOS可以通过file快速查看1.png


$ file 1.png 
1.png: PNG image data, 188 x 196, 8-bit/color RGBA, non-interlaced
复制代码




  • width位于第16个byte,长度是4bytes



  • height位于第20个byte,长度是4bytes


const getPNGSize = buffer => {
  let w = 16;
  let h = 20;
  return {
    width: buffer.readUInt32BE(w),
    height: buffer.readUInt32BE(h)
  };
};
复制代码

buffer又是什么?


我们简化一下base64图片格式,还是以png为例讲解


data:image/png;base64,CODE
复制代码

对base64编码的图片字符串,解析,获取到CODE内容,然后使用Buffer.from转换为'base64'编码的buffer


import {Buffer} from 'buffer';

const buffer = Buffer.from(CODE.toString(), 'base64');
复制代码


vscode还可以使用Hex Editor[7]插件,能够更快捷的查看转码后的内容,同时也能够帮助buffer的转换提供一些思路。hexdump[8]需要鼠标hover才会提示。






image.png


其他图片格式,同理可得!!(???说的好轻松???)


例如gif文件





image.png


const getGIFSize = buffer => {
  return {
    width: buffer.readUInt16LE(6),
    height: buffer.readUInt16LE(8)
  };
};
复制代码




image.png


DB


数据存储,使用SQLite,足够轻量,简单易学易用,需要引入`sql.js`[9]


功能介绍

该部分在db模块,基本涵盖的功能可以概括为:





  • 数据库的初始化、读取、存储、重置



  • 数据表的初始化、查询、插入、更新和删除



  • 获取某表的一条数据



  • 获取某表的所有数据



  • 获取所有数据



  • 日志


表结构设计

表结构,目前设计了四张表





  • STORY 记录图片指令和base64的image



  • TEXT 记录图片对应的绘图信息,例如x, y, font, color等



  • LOGGER 日志表,主要收集imeme缺失的资源



  • SPECIAL 特殊表,表结构同STORY,用于保存彩蛋指令,像中秋节、国庆节这种关键字,Chat端通过@imeme是查询不到的,属于隐藏的key,使用@imeme 中秋 金馆长会随机返回一张图。


CREATE TABLE STORY (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  mid CHAR(50) NOT NULL,
  title CHAR(100) COLLATE NOCASE,
  feature CHAR(100) COLLATE NOCASE,
  image TEXT NOT NULL
)
复制代码




  • id 主键,自增,不用于其他操作



  • mid 唯一key,用于数据的各种操作



  • title 文件标题,图片指令,唯一



  • feature 所属类别,用于归类,很多title可以对应同一个feature



  • image base64 image


不同于MYSQL,由于SQLite是大小写不敏感的数据库,所以为省去后面使用上的麻烦,建表的时候,把所有字段都统一小写。


数据备份

灾备的话,目前仅提供基于脚本的方式备份数据,npm run backup,默认把常用表和特殊表的内容,转化成js文件,存储到指定位置,默认为assets/backup目录。(后续会支持数据库自动备份)


数据采集

提供两种方式的数据导入





  • npm run import fileName默认读取assets/fileName目录,获取满足格式要求的文件,转换为base64,并附加绘图基本信息,存储为fileName.js文件。





  • 交互式添加单个图片,自定义表情内容,支持选择、拖拽以及拷贝粘贴的方式添加新图。




图形界面


为了更加友好方便快捷的斗图,imeme需要配备一个管理端imeme-view,它主要做这些事:





  • 管理数据源,管理imeme所有的表情资源



  • 查看表情



  • 动态调整绘图参数,支持可拖拽本文编辑,实时查看



  • 新增表情,提供选择框,拖拽、拷贝粘贴三种方式导入



  • 下载,实时下载表情资源





image.png


部署

前端静态页依赖于Gitbhu Action[10]托管在Github Pages[11],Node Server部署在Vercel[12]


vue3 + vite

<script setup lang="ts">谁用谁知道,爽的不得了。


lib

为了便于imeme的任意部署和运维,提供imeme-view的lib输出,支持在多种(es/cjs/umd/iife)环境下的使用。


主要依赖于强大的vite + rollup。





  • npm run lib 构建生成各种格式的js库



  • vite.lib.config.ts 配置文件,指定基础构建目录和打包方式



  • .env.lib 环境变量



  • lib/index.ts lib包入口,提供load方法,用于加载替换DOM元素和提供服务的url地址



  • lib/index.html 使用示例


npm使用引入meme-view[13]


成长


精疲力尽,受益匪浅。





  • 成长的路,如果有能够一起奋斗的伙伴固然难得,在大家做项目产品的团队中,与peer保持良好的合作关系,当我们遇到问题,就能够很方便求助解答,专业问题交给专业的同学(感谢2geng[14]同学在专业领域给予的大力支持,希望他的第一篇博文再快些)。





  • 做好时间管理,前前后后用掉很多碎片时间,通勤的路上思考,半夜睡不着爬起来赶进度,放弃午睡,每天花一点点时间,努力搬砖。





  • 脚踏实地,慎始敬终,行稳致远,进而有为。




结语


有好的idea,就动手行动,不要让idea就是一个idea。


意见收集


大家如果想要什么表情,可以自己加,也可以留言,看到后会及时补充。更欢迎提交pr,提交issue。


还有一些功能在不断的丰富和完善。





  • [ ]  解决Web端canvas绘制gif不动



  • [ ]  增加gif格式的水印服务



  • [ ]  数据的定时备份



  • [ ]  数据源的下载



  • [ ]  资源内容太少,缺少欢迎新人系列、大胆想法系列,撤回也没用等等



关于本文


来源:水鳜鱼肥


https://juejin.cn/post/7018395454962401288


















往期推荐
















大厂面试过程复盘(微信/阿里/头条,附答案篇)











面试题:说说事件循环机制(满分答案来了)












专心工作只想搞钱的前端女程序员的2020






























最后














  • 欢迎加我微信,拉你进技术群,长期交流学习...


  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...























点个在看支持我吧












浏览 45
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报