多此一举生成器 - 从开发到使用
为什么做这个工具
上次想到这么无厘头的项目还是在上次,今天给大家分享一个非常具有废话文学精神的项目 ——《多此一举生成器》。
想到做这个项目的起因是在跟朋友聊天的时候,经常截图自己发的话来发言,大概就是这样:

每次都要输入一遍并且发送出去才能截图,十分的不方便,所以就想到是否能做一个工具,可以在输入文字后自动生成一张聊天截图。
在地球上,每过 60 秒,就过去了 1 分钟,事不宜迟,让我们马上开始吧。
生成截图
初始化项目
我们将使用 Node.js 来开发这个工具,首先新建工作目录并初始化 npm 项目:
1 | mkdir redundant-tools && cd redundant-tools |
npm 安装 canvas 包,让 Node 支持 canvas 绘图:
1 | npm i canvas |
tips:因为 canvas 包需要根据本地 Node 版本编译原生模块,所以需要 gcc 环境,如果环境不完整可能会安装失败,这时候需要根据 官方( https://www.npmjs.com/package/canvas )指示安装对应环境。
新建 index.js:
1 | const { createCanvas, loadImage } = require('canvas'); |
分析截图构成
一张截图由 背景、头像、气泡、聊天文字 组成,其中背景和气泡颜色会根据 主题(dark/light) 变化。


首先在绘制之前,我们需要知道最终绘制出来的截图尺寸,用于设置 canvas 的宽高。
影响截图宽高最重要的因素就是聊天文字的内容,文字内容多,则宽度更宽,当到达一定界限后,文字内容会换行,换行以后就会影响截图的高度了。
计算高度
截图的最终高度 = 截图内边距(20px * 2) + 气泡内边距(17px * 2) + 行数 * 行高(42px)。
那么怎么知道聊天内容文字的行数呢?
canvas 的 context 提供了一个 measureText 方法,用于测算文字渲染的宽度。入参为文字内容,会返回一个只有 width 的 object。
tips:字体 family、size 都将影响最终测算结果。
下面是仿 Mac OS 微信客户端聊天文字样式的文字行数及高度测算:
1 | const chatLineHeight = 42; |
计算宽度
截图最终宽度 = 截图内边距(20px * 2) + 文字内容宽度 + 气泡左内边距(24px) + 气泡右内边距(26px) + 头像大小(64px) + 气泡与头像距离(10px)。
因为我们设置的单行最大宽度为 830px,小于等于 830px 的文字只有一行,超过则为多行,那么可以判断 lines 的 length,大于 1 的文字宽度取 830px,小于 1 的取 measureText 方法返回的 width。
1 | const bubbleRightMargin = 10; // 气泡和头像的距离 |
得到真实宽高以后重新设置 canvas 的尺寸:
1 | // 重设 canvas 大小 |
工具函数
这边准备了几个常用的 canvas 绘制工具函数:
1 | // 图片填充模式 |
下面开始绘制截图啦~
背景
绘制一个跟 canvas 一样大的方形,填充主题背景色。
1 | // 绘制底色 |
气泡
原来想的是用安卓 .9.png 的思路,做一个可拉伸的图片来实现气泡,根据文字内容伸缩尺寸,如下图:

然后突然格局打开了,气泡不就是一个圆角矩形加一个三角形么,然后就更换了实现方式。
首先需要知道我们需要画多大的圆角矩形,在哪里画这个圆角矩形。
圆角矩形的尺寸计算跟 canvas 尺寸计算如出一辙:
1 | // 气泡大小 |
绘制的起点则取截图内边距(20px,20px):
1 | // 绘制气泡背景 |
然后给气泡加上箭头,箭头是由 png 图片转换的 base64 数据,使用 canvas 提供的 loadImage 方法加载,需要计算出绘制的位置,代码如下:
1 | const arrowImage = await loadImage(arrowBg); // 加载箭头图片 |
这样我们就得到了一个预留了头像位置的聊天气泡。

头像
头像是可配置的,方便其他同学使用的时候换上自己的头像,所以头像的地址将从运行时的环境变量中获取。然后同样使用 loadImage 加载,在指定位置绘制。
头像的横向起始位置 = 截图总宽度 - 截图内边距(20px) - 头像大小(64px)
头像的纵向起始位置则取截图内边距(20px)
1 | // 头像横向位置 |
这样头像就加好啦,万事俱备,只欠写字。

聊天文字
上面我们已经进行了聊天文字的分行,现在只需要将文字逐行绘制在气泡中,就完事儿了。
1 | // 绘制聊天文字 |
这是绘制一行文字的截图:

这是多行文字的截图:

保存文件
绘制完的图片还在 canvas 中,我们需要将它保存到本地,可以使用 canvas 提供的 toBuffer 方法,将内容输出为二进制流。然后结合 Node 的 fs 模块即可完成保存。
1 | const fs = require('fs'); |
至此,《多此一举生成器》的功能已经开发完成。
便捷使用
让我们尝试在项目根目录下运行:
1 | headUrl=https://fmcat-images.oss-cn-hangzhou.aliyuncs.com/head.png projectPath=. node index.js 这是黑色主题的文字 dark |
这样在根目录下就生成了一张名为 output.png 的图片,在聊天工具中选择图片就能发送了。
但是这样使用起来太繁琐,一个不够便捷的工具不能算一个好工具。
如果能输入聊天内容后直接生成图片,并把图片复制到剪贴板,然后直接在聊天软件粘贴岂不美哉?
为了实现上面的想法,需要引入工作流工具。
工作流
工作中常见的的工作流工具有 Jenkins、阿里云效中的流水线、GitHub Action 等等。配置完成后可以通过特定动作、时间触发流程,完成指定任务,比如项目发布、构建部署、产品输出等等。
这些工具大部分都是在远端环境,无法操作本地环境的剪贴板。
我们将使用一些更便捷,能够和操作系统结合紧密的个人工作流工具来达成目的。
这边仅介绍 Mac OS 下的工具,使用 Windows 的同学可以自行探索。
Alfred
这可以称得上是 Mac OS 上最能提升效率的工具,提供文件、软件搜索比系统自带的更好用,还有剪贴板历史记录、快捷计算器等等便捷功能。
它最强大的功能当属 Workflows,用户可以自己编辑流程,直接支持多种语言,不支持的语言可以通过 bash 运行,开发完的工作流可以导出文件用于分享。社区繁荣,GitHub 上有很多开源的 Workflows 项目可以使用、学习。
比如有道翻译,导入工作流文件以后,配置有道官方申请的 key 和 secret,使用设置好的快捷键唤起输入框,输入要翻译的文字即可。

心动不如行动,下面我们使用 Alfred Workflows 来配置一个能够便捷使用《多此一举生成器》的工作流。
外部变量
在开发时预留的外部参数分为 运行参数、环境变量。
让我们再来回顾一下运行工具的命令:
1 | headUrl=https://fmcat-images.oss-cn-hangzhou.aliyuncs.com/head.png projectPath=. node index.js 这是黑色主题的文字 dark |
运行参数
chatText:生成截图的聊天文字,定义为命令中 index.js 后面的第一个参数。
theme:截图的主题,将影响背景色和气泡颜色,定义为命令中 index.js 后面的第二个参数。
通过下面的代码获取外部传入的值。
1 | const [chatText = '', theme = 'light'] = process.argv.slice(2); |
环境变量
相对于运行参数,环境变量的值不需要频繁变化,只需要在项目配置时设置一次。
projectPath:项目路径,用于指定 index.js 和截图输出的 output.png 文件位置。
headUrl:设置截图中用户头像的 URL 地址。
新建 Workflows
打开 Alfred 主面板,选择 Workflows 选项卡,新建一个使用关键词触发运行命令的模板。


注意 Bundle Id 需要唯一,后续更新时会判断是否为同一个 Workflows。
右上方可以设置图标,让你的 Workflows 更具辨识度。
设置环境变量
在编辑区的右上方有个 [χ] 按钮,可以打开环境变量编辑面板。

点击右下角的加号按钮依次添加 headUrl、nodePath、projectPath 环境变量。
这边需要 nodePath 是因为 Alfred 的默认 bash 环境中找不到 node 命令所在地址,需要我们手动指定一下。
设置关键词
双击 Keyword 流程块打开详情编辑面板。

保存后在 Alfred 输入框中输入 dd 就能看到我们的命令了。

根据主题不同,可以设置两个关键词。

接着在空白区右键,新建一个变量处理块,在下方变量列表中新建一个 theme,值为 dark,与 dd 关键词连接。
Argument 中的 {query} 为前方 keyword 块截取的聊天文字。
变量处理块会将内容带给下一个流程块。

同理,theme 值为 light 的变量块与 dl 关键词连接。

判断
项目目录未设置、用户头像未设置都不能顺利运行工具,所以需要提前判断,通知用户进行设置。
在编辑区空白区域右键新建 Conditional 块。


通过 {var:变量名称} 语法取环境变量中的 projectPath 和 headUrl,判断条件满足会对外暴露一个流程端口,用于连接后续的流程。

在变量为空的接口上连接了一个调用通知的块,可以通过空白区域右键新建,填写对应的通知文本。
现在我们把环境变量中的 projectPath 置空,再次运行工具,将会得到一个提醒设置项目路径的通知。

运行脚本
当 projectPath 和 headUrl 都非空时,会走 else 的端口,后面再连接一个对 nodePath 的判断,两个端口连接的都是 Run Script 块,用于运行代码。

下面对比一下两者有什么区别:

当 nodePath 为空时,会先执行 source ~/.zshrc ,因为目前 Mac OS 默认的终端是 zsh,且假设使用工具的人都是同行,配置了 nvm。如果有不同环境,请自行配置。
当 nodePath 非空时,执行代码如下:

手动配置 nodePath 后运行速度会比自动引入更快,这边还是建议手动配置一下。
然后就是跟手动运行项目一样的命令,只是命令中的部分内容被替换成了前面配置的变量。
取环境变量为 $ 开头,后面跟环境变量名称,此处的 $1 为上一个块传进来的 query 。
更进一步
完成上面的运行工具就能在项目根目录生成聊天截图了,但是要使用这张图片还是很不方便,需要到项目目录下复制。
能不能把生成的图片直接复制到剪贴板呢?答案是可以的,但是实现的过程很曲折。
首先考虑的是能否使用 Node 来完成这步操作,翻阅文档并没有相关的 API,在 npm 上搜索了很久,只找到能操作文字复制到剪贴板的包。
当我要放弃的时候,想到了 AppleScript,这是苹果自家的脚本语言,可以用于调用系统 APP 完成指定功能。
翻阅文档发现 Finder 有相关的 API 可以实现复制文件到剪贴板,实现起来也很简单,只需要知道文件的绝对路径,就可以完成操作。
我们先使用变量处理块把图片的绝对路径拼接好,传给代码运行块


代码运行块中代码如下:

运行时 {query} 会被替换成拼好的图片绝对路径,这句代码的意思是执行 AppleScript,告诉 Finder 将指定图片设置到剪贴板。
代码运行块后面再连接一个通知块,通知用户图片已经生成完成并且复制到剪贴板了。
流程总览如下。

现在我们只需要唤起 Alfred 输入框,输入 dd/dl 聊天文字 ,然后回车等待通知生成完成以后就可以在聊天工具中粘贴发送了。
快捷指令
Apple 于 iOS 11 添加了 捷径 App,后改名为快捷指令,用于实现一些流程化操作,大大提高了 iOS 的可玩性。
如今 Mas OS 12 上也支持了快捷指令,我们可以使用快捷指令来配置《多此一举生成器》,跟 Alfred 的配置方式大同小异,只是在变量设置、获取方面有一些不同。
编辑

运行
将编辑好的快捷指令拖动到菜单栏文件夹,在菜单栏中就可以看到快捷指令的图标了。

点击即可运行,首先会弹出一个选择主题的弹窗,选择后点完成。

接着是要求输入聊天内容的弹窗,输入后点击完成,就开始执行工具脚本了。

执行完成会收到通知。

最终生成的聊天截图:

小结
虽然《多此一举生成器》这个项目没有很大的实用性,只是一个无厘头的搞笑项目,但还是有一些技术含量的。
用好工作流工具,能让你事半功倍,可以把多种技术结合起来,把不可能变为可能。不论是自己写小工具还是团队项目开发,都能有用武之地。
目前项目已经开源,附带了配置好的 Alfred Workflows 和 快捷指令链接,朋友们可以开箱即用。项目地址在下面:
1 | https://github.com/RongleCat/fmcat-redundant-generator |
好了朋友们,马上就要涨潮了,下次再见~




