点击上方 前端瓶子君,关注公众号
回复算法,加入前端编程面试算法每日一题群
有一天,组织内的斗图机器人坏掉了,巧不巧的是当你需要用它时,它坏掉了。
赶上要催交同学们的周报,没有表情包,就没办法委婉又不礼仪并友好和善的催促同学们交周报。
然后只能自己做图,打开了度娘,找合适表情,然后打开sketch,一通操作后,粘贴到群,搞定。
but总使用同一表情,又很枯燥,于是又打开度娘,打开sketch,一通操作,粘贴到群,搞定。
过了一段时间,度娘,sketch,群。
又过了一段时间,度娘,sketch,群。
时间一长就会很烦躁,每次都要这样搞半天(难道喜新厌旧属性?感觉像渣男?)
后来,突然开朗。求人不如求己!发挥我的主观能动性!自己敲一个!
于是在经历Node的洗礼,Color的洗礼,Canvas的洗礼,SQL的洗礼,Docker的洗礼,Vercel的洗礼后,它诞生了。
它叫imeme,是一个斗图机器人。
给大家介绍如何设计和实现一款斗图机器人,是有前端有后端的全栈开发。
涉及到安全问题、隐私以及制度政策等原因,机器人的接收消息内容不介绍
具体功能演示,不提供截图展示,可自行体验
不会详细讲清楚每一个实现细节
But,这些限制要素无关紧要,不影响全局,也不影响大家搭建自己的机器人。
机器人的技术选型
关键环节的设计思路及相关知识点。
使用markdown还原下真实交互场景
image.png明确目标,鼓舞斗志。
那么应该如何设计主体流程?先从最基本的功能入手,列下需求清单:
Server,用来接收命令,发送消息。
绘图功能,能够把文字和图片做成一张图。
图片处理,不同的图片类型采取不同策略,获取最基本的图片信息。
数据存储,作为数据源,提供各种有意思的基础图片及与绘图相关的基本参数。
录入导出,便于数据采集,迎支持插入多条数据以及数据库的备份。
UI,让imeme用起来更轻松,便于管理数据源,查看图片以及调整绘图参数,还应支持交互式新增和图片下载。
针对如上特点:
Server端,基于express实现node服务,axios + canvas + sql.js。
UI端,vite + vue3.x + typescript设计实现,并提供lib库供多端快捷接入。
简单怼了一张图
image.png界面管理就很常见了,大致长这样
image.png所有源码,链接在文末参考资料中,在github上。服务部署到vercel,可访问体验Web端[3](网速不稳定,毕竟白嫖vercel)
Server要实现,接收到消息命令请求后,绘制图形,并能够给出合理结果反馈,也就是新的图像。所以基于express实现node服务,接口的设计要求如下:
/test
用于测试服务的可用性,get请求。
设置origin *
允许接口的跨域请求以及多种请求头,默认编码utf-8。
为Chat端提供的/send
,post发送Webhooks消息体。
为Web端提供的/image/*
接口
/catalog
用于目录获取,读取数据库中存储的图片源列表显示。
/open
打开用户选中的列表内容,接口返回图片基本信息(base64及绘图数据)。
/save
绘图数据的保存接口,用于图片拖拽编辑后,把最新数据同步到数据库中。
/create
新建表情,保存到数据库。
/update
更新表情数据
/download
下载接口,用户拖拽好的内容,可以直接下载到本地。
/export
数据导出备份
Server的接口逻辑在service
模块,分为四个层次
router.js
api接口层,管理服务提供的所有api。
data.js
连接接口和数据库的数据层,数据封装,为api提供数据获取服务。
ajax.js
请求结果集封装,根据data.js
请求,给出结果反馈信息。
send.js
为Chat
端提供的发送消息服务
简单的讲,表情就是图片加文字,即我们常见的水印,选择使用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]
在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');
复制代码
image.pngvscode还可以使用Hex Editor[7]插件,能够更快捷的查看转码后的内容,同时也能够帮助buffer的转换提供一些思路。hexdump[8]需要鼠标hover才会提示。
其他图片格式,同理可得!!(???说的好轻松???)
例如gif文件
image.pngconst getGIFSize = buffer => {
return {
width: buffer.readUInt16LE(6),
height: buffer.readUInt16LE(8)
};
};
复制代码
image.png
数据存储,使用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所有的表情资源
查看表情
动态调整绘图参数,支持可拖拽本文编辑,实时查看
新增表情,提供选择框,拖拽、拷贝粘贴三种方式导入
下载,实时下载表情资源
前端静态页依赖于Gitbhu Action[10]托管在Github Pages[11],Node Server部署在Vercel[12]
谁用谁知道,爽的不得了。
为了便于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
欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!
回复「阅读」,每日刷刷高质量好文!
如果这篇文章对你有帮助,「在看」是最大的支持
》》面试官也在看的算法资料《《
“在看和转发”就是最大的支持