从0开始开发一个picgo插件
banner:https://www.pixiv.net/artworks/92048077
前言
嘛,正如标题所言,我确实打算写一个picgo插件。
并开此贴为证,(那如果写了个bug岂不是贻笑大方),记录一些过程(破事水)。
PicGo: 一个用于快速上传图片并获取图片 URL 链接的工具
简单来说, picgo 可以把图片上传到各个图床,获取外链,方便在Markdown、BBS等各种场合使用。
为什么要使用图床
图床就是储存图片的网站。现在的图片动辄上几 MB ,使用图床能够有效提高图片的加载速度。有些论坛对于图片使用有大小限制,像很多 discuz 论坛个性签名的 gif 就只有用图床才能做到。此外,图床还能对图片进行备份。博客是可以长期存在的东西,但图床就不一定了。在本地、 github 、图床三端存储,可以把图片丢失的概率降到最低。
使用流程
没有 picgo 时:
有了 picgo 之后:
- 下载图片到本地
- picgo上传并压缩
- Markdown 中插入链接
可以看到直接砍掉了中间几步,大幅提高工作 摸鱼 效率。
旧事
大概也是去年的这个时候,我根据 插件开发指南 写了一个 Ucloud 的 uploader 。如果以二零年九月为一条线,向前向后可以分出两个我:前一个我,什么都不懂;后一个我,也什么都不懂。
呃,为什么上面的图是 Rabbit 而不是 Bunny Girl
总的来说,这工具挺好用。没想到的是,发布到 npm 的一年的时间里,竟然还有几个人去下载。那为什么 github 只有三个 star
而个人在使用中,觉得还有一点不足:当图片在剪切板中或者用图片的 url 上传时,只上传到了图床,如果要保存到本地,还得手动操作,略显麻烦。于是就想再开发一个插件。于是就找了找以前的项目。于是就看不懂自己之前写的了。
开发
由于本人智商孱弱,只得跟着文档重新一步步来。
确定需求,明确插件的生命周期
需求上面提了:在剪切板中或者用图片的 url 上传时,能够自动保存到自定义的文件夹。而整个上传的生命周期(Lifecycle)如下:
理论上,我们可以从3个生命周期钩子中任选一个来注册我们的插件,但是考虑到其他插件,情况会复杂一点。比如我有一个压缩图片的 Transformer ,那到底是保存压缩后的文件还是压缩前的。虽然可以让用户自定义,但一般来说,我们对保存在本地的图片的大小是宽容的,追求清晰,所以可以注册一个 beforeTransformPlugin 。
准备环境
- 安装 node 。
- 安装 picgo 。
npm install picgo -g
- 试运行。
picgo -h
使用插件模板
为了方便开发者快速开发picgo的插件,PicGo官方提供了插件模板。然后就傻瓜式操作了。
1 | PS D:\repo> picgo init plugin picgo-plugin-store |
此处我要说两句, TS 是 JS 的超集,TS 相对于 JS 增加了许多功能,picgo 就是用 ts 写的,而且上面也提示了 ts 是 recommended 的,所以,我们这里就直接选 js 吧,当然是因为这种 fragments 用 ts 完全是杀鸡用牛刀,才不是因为我一点都不会用 ts 呢,不是!
1 | ? Use TS or JS? js |
接着按提示 cd && install 。
代码编写
初体验
模板的代码已经为我们搭好了简单的框架,我们只要把逻辑在 handle 中实现就行了。
1 | const handle = ctx => { |
接下来我们先测试一下。由于我们要开发 GUI 插件,这里一步到位了,所以 cd 到PicGo默认配置文件所在的目录下,输入:
1 | npm install D:\repo\picgo-plugin-store |
然后重启 PicGo ,就能看到自己的插件了。
我们在 handle 中加几行代码:
1 | const handle = ctx => { |
然后再重复执行
1 | npm install D:\repo\picgo-plugin-store |
并重启,试着上传一张图片,就能在弹窗中看到输入的数组了。话说回来,这样调试起来还是挺费劲的,代码更新后就要 install ,没有断点,没有热重载。在官方文档看了一下,好像并没有调试的详细步骤。如果想要调试 GUI ,可能还要 clone PicGo-electron 。下面是一种在 PicGo-Core 中能断点的方法:
- 在你插件的代码中新建个文件,比如
src/test.js
。 - 写上代码调用picgo。
1 | // 系统全局安装路径,用 npm config get prefix 查看,在其下的 node_modules 里。 |
- 在
package.json
中修改 test script :
1 | "scripts": { |
这样就能在 IDE 中 debug 了。
开始上路
Transformer的作用是把input输入的内容(比如path)转化成Uploader可以上传的内容。
那input数组的每个输入元素到底是什么呢?经过几次上面方法的测试,我们可以得出结论:对于本地图片,就是本地路径;对于剪贴板,先把图片保存在本地临时文件夹中,因此是一个本地临时文件的路径。然后再通过回调,在整个生命周期结束,或发生错误时删除图片。
关键代码如下:
1 | // 这个函数就对图片在剪贴板 |
那如果从 url 上传图片呢?再读读源码就知道,官方的默认实现是在 path 这个 Transformer 中,从网上拿数据。
1 | if (isUrl(item)) { |
考虑到我们插件的生命周期在 Transformer 之前,所以我们应该提前先拿到数据,保存在本地。而这样一来图片就会被下载两次,所以我们可以修改 input ,使之变为本地文件的路径。如果是本地上传的文件,我们只要拷贝一份到目标目录就行。如果本地文件不需要重复存储,只要把插件配置的目标路径设为本地文件所在的路径即可。
写bug
主要逻辑:
1 | const storeHandle = async (ctx) => { |
copyFile
返回的就是item
,而downloadFile
会返回一个带文件名的,和配置的路径一致的绝对路径。对于默认命名规则,剪贴板是时间戳, url 则是其最后一个路径。有时我们想使用自己的名字。于是我试图这么做:
1 | if (config.setEachImgName) { |
但是很遗憾,作者并没有给插件的 handle 方法提供 guiApi ,所以我们自定义名称这条路就行不通了。。。吗?
看看设置,很容易找到一个上传前重命名。
那再看看代码,看我们能不能拿到这个名字
1 | picgo.helper.beforeUploadPlugins.register('renameFn', { |
从中我们可以看到几个关键点:
- 作者也是注册了一个插件。
- 改名插件的生命周期在我们自己插件的后面。
- 改名通过修改 output 中 item 的 fileName 来实现。
那我们就可以再注册一个 afterUploadPlugin ,在上传完成后对目标目录内的图片进行重命名。
1 | const renameHandle = ctx => { |
这样,我们就完成了基本的代码逻辑。
部署
官方的插件模板已经为我们准备好了.travis.yml
,我们只需要配置好NPM_TOKEN
就能在推送到 github 时直接用 Travis 发布到 npm 上了。 所以我直接把 嘛,github 官方的稳定性应该比 Travis 好一点吧。。.travis.yml
删了,换上了 github action。
配置Action
新建.github/workflows/npm-publish.yml
,写入:
1 | on: push |
为了下面自动打 tag ,我们修改package.json
中的 version 为 0.0.0
。接着就是一套提交流程:
1 | git init |
如果你在项目中设置了NPM_TOKEN
(可用 npm cli login 后在 .npmrc
文件中找到。),那么应该就能看到 npm 上自动发布的包啦~
后续维护
如果修改了本地代码,只需:
1 | git add . |
就又能提交代码并自动发布到 npm 上啦!
后记
没想到简单的一个项目写的还挺长。看了一下 picgo 可以追溯到2018年。 怪不得还在用已经 Deprecated 一万年的 代码中也可能会有点历史遗留问题,比如看起来就很鸡肋的 base64 Transformer。当我看到这段代码时表情是疑惑的:request
。
1 | import { IPicGo } from '../../types' |
又仔细看了看文档,读了一下源码:
才明白默认实现没用到这个,对照代码确实base64 将直接接收一个Uploader可支持的output数组
,也明白了 buffer 和 base64 二选一原来是对 Transformer 来说的,因此对于 Uploader 理论上要把两个都实现一下。写到这里我突然想起来我之前写的 ucloud-uploader 好像只实现了 buffer ,还有一个图片名是中文时 encodeURI 的 bug 没修。
但因为这几天已经码了不少字,所以明天再改吧。