Cloudflare教程(一)- Wokers

Cloudflare教程(一)- Wokers

需求背景

一直以来,我都很热衷于在网上薅各种免费api的羊毛,看到有啥好的api就想着拿来做个啥东西,正好最近在公司也不算忙,我就想着,薅Cloudflare的羊毛这么多了,是该帮它做做宣传了(其实就是想多研究研究Cloudflare,看看还有啥能继续薅的)。😬所以,开始了一边研究Cloudflare的各种功能,一边写博客记录一下,免得以后又忘记怎么用了。

今天就从Cloudflare的Wokers开始,毕竟它足够灵活,使用js代码来处理逻辑,背后还有D1 数据库,KV数据库和R2对象存储,这仨都是免费的(当然是一定用量之内),那不得完全用起来啊,不然就亏了。

至于需求,之前在n8n做了每日AI新闻的功能,这个是依赖了ChinaHolidayAPI这个开源项目提供的api接口直接调用的,我就打算把这个接口自己来实现一遍,一来是学习使用Workers,二来也可以给其他人提供一个接口使用(这当然就是借花献佛了),当然我也不打算直接做接口的转调,而是自己存储ChinaHolidayAPI项目提供的json文件,然后用js实现解析日期,根据json内容返回是否工作日等数据。

最终实现的效果:
n8n自动从ChinaHolidayAPI仓库获取当年的节假日json数据存入R2对象存储,将数据存入D1数据库中,wokers从D1数据库中获取数据,根据数据判断当前日期是否是工作日,并返回结果。当然,查询过的日期,也同时存入KV数据库中,接口也优先从KV数据中查找数据。

当然,这里必须要感谢ChinaHolidayAPI项目的作者,点赞。

技术选型

  • Cloudflare的Wokers
  • ChinaHolidayAPI
  • D1 SQL数据库 – 这相当于MySQL
  • Workers KV数据库 – 这相当于redis
  • R2对象存储 – 这相当于OSS,我准备拿来存节假日的json文件

搭建过程

n8n获取当年数据存入D1数据库

创建n8n工作流,定时判断仓库是否更新

实现思路是使用GitHub的api接口,查询如:2025.json的内容,当然GitHub的接口返回了该文件的sha值,可以直接比较该值和缓存的值是否相同,就可以判断其是否更新过了。
这里不得不提一下,我也是最近才了解到GitHub提供了非常丰富的api接口,可以查询仓库的各种东西,比如某个文件内容,某个issue的修改时间等等,我也是利用这些接口,做了一些小工具自己使用,比如我给某个仓库提了issue,但是我并不想一直盯着作者是否回复了我的issue,我就会用n8n定时去查询是否更新,有更新了就通知我。

此功能的文档:https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28

下图是我使用postman测试该接口返回的数据,可以看到,GitHub甚至返回了文件内容,只是是进行了base64编码。

不够我并不打算直接使用content字段的内容,它里面包含了\n这种的换行符,我不想去处理它。观察接口返回内容,我发现有一个download_url字段。
“download_url”: “https://raw.githubusercontent.com/Dreace/ChinaHolidayAPI/master/holidays/2025.json

可以看到,这个url直接返回了文件内容,没有换行符,也没有编码。咱直接拿着就能用。

下面是我的n8n工作流的截图,此时只是实现了拉取文件内容,并且进行分割,也准备好了loop,准备写入D1数据库。还有很多判断,比如文件sha值判断,将文件内容缓存进R1对象存储没有去完善。其实缓存文件到R1对象存储,并没有多少用处,我也只是想试试看R1的api调用方式。

处理数据为obj这一步,其实就是为了方便后面循环将数据写入D1数据库准备的,把数据转为列表。

这样子处理之后,循环中拿到的数据就会如下,很方便用于处理,到时候写入D1数据库中甚至KV的时候,date作为主键或者key:

1
2
3
4
5
6
7
[
{
"date": "2025-01-01",
"type": "假日",
"note": "元旦"
}
]

关于我为什么要把数据写入KV这一点,我其实将其理解为传统后端开发中的redis,D1作为MySQL,那么优先从redis中取数据,肯定效率更高,其次再去MySQL查数据,当然也要同步到redis中去。

我提供了当前这个时间节点的工作流json,后续就不提供了,里面会包含我的cloudflare的token等信息,我也不想去处理敏感数据。

节假日数据采集json

缓存文件sha值

创建一个KV数据库实例,我这里叫做n8n-杂项,我是准备将一些小工具的数据都缓存进这里。名称右边这个就是库的id。

创建D1数据库,我起名为n8n,准备n8n相关的东西都存在这个库里。新建一张表,叫做 holidays 。

1
CREATE TABLE IF NOT EXISTS holidays (id INTEGER PRIMARY KEY AUTOINCREMENT,date TEXT, type TEXT, note TEXT);

表结构很简单,一个id作为主键,并且自增。另外就是日期、类型和描述。

特别提示一下,cloudflare的api请求,可以参考其文档。都需要添加两个Header,一个是Authorization,一个Content-Type。
Authorization: Bearer 你的api令牌(cloudflare->管理账户->账户API令牌),自己去新建一个,权限给KV数据库就行
Content-Type: application/json

KV数据库的api调用方式:

https://api.cloudflare.com/client/v4/accounts/你的账号id/storage/kv/namespaces/KV数据库的id/values/要查询的key

可以看到,第一请求的时候,没有缓存,返回是报错信息。在HTTP节点之后,添加if节点,判断状态码是否为404,如果是,则说明没有缓存,进行缓存。

进行缓存的api(注意这里api使用的是PUT方法):
https://api.cloudflare.com/client/v4/accounts/你的账号id/storage/kv/namespaces/KV数据库的id/bulk

这里要添加Body,格式如下:

1
2
3
4
5
6
7
[
{
"key": "{{ $('获取当年json').item.json.name }}",
"value": "{{ $('获取当年json').item.json.sha }}",
"metadata": {}
}
]

截图如下:
这里我从其他地方copy了缓存sha值的节点过来,但是忘记改节点名字了,【缓存修改时间】就是【缓存sha值】节点,后续截图中也都是错误的。

最终我们就能在cloudflare后台,查询到该缓存。

判断文件是否更新

添加一个 Extract from File 节点,将KV数据库返回的内容提取出来(我不知道是我的姿势不对还是怎么滴,反正cloudflare返回的内容居然是个二进制文件,所以这里得再提取一次)。

可以看到,有缓存之后,走到了if的false分支,现在没有变动。

提取返回内容:

判断是否变动:

数据存入D1数据库

现在已经可以判断文件是否更新了,如果更新了,就进行数据写入D1数据库。我们将if判断的true分支,连接到之前处理数据的节点。并且在没有缓存的时候,也需要走数据处理逻辑。这一步之后的工作流如下,你的数据可能从图中箭头的两个链路执行:

现在剩下的就是将数据写入D1数据库了。

写入D1数据库的api:

https://api.cloudflare.com/client/v4/accounts/账号id/d1/database/D1数据库id/query (POST请求)

body如下:

1
2
3
4
{
"sql": "INSERT INTO holidays (date, type, note) VALUES (?, ?, ?)",
"params": ["{{ $json.date }}", "{{ $json.type }}", "{{ $json.note }}"]
}

在cloudflare后台,可以看到数据已经写入了D1数据库。

特别注意,你的apitoken,记得要有D1数据库的权限。否则可能报错。

当前的工作流截图:

优化容错

这个工作流有几个问题:

  1. 获取当年json文件的时候,如果该文件不存在,则工作流会报错中断。 如:2026年了,但是仓库作者如果更新不及时,2026.json文件并不存在,那么工作流会报错;
  2. 如果作者更新了某一年的文件,那么sha值也会改变,就会造成当年的数据写入两份到数据库中。

优化方式:

  1. 其实中断流程,也不影响数据,倒是没有多大问题,但是如果能通知到我肯定更好;
  2. 写入某年数据之前,删除对应年的所有数据,然后全部重新写入一遍。

进行优化

优化该年文件不存在的情况

在获取【当年json】节点后添加if判断,判断正常返回时的name字段是否存在,如果不存在则通知。

这里要记得修改,否则该节点报错之后,不会继续执行if节点,就不会有通知。要让节点报错继续执行后续节点。

当前处理流程:

某年数据写入前,清空该年现有数据

这个很简单,只需要在开始写入数据之前删一下数据即可。

1
2
3
{
"sql": "DELETE FROM holidays WHERE date LIKE '{{ $('当前年份').item.json.datePart }}-%';"
}

把写入数据库的HTTP请求节点拷贝一下,放到写入之前,统一删除该年的数据。

总结

鉴于篇幅,这里就把workers的搭建放到下一篇文章了,目前我们将数据的准备算是做完了。我们已经能够在D1数据库中查询到完整的数据。后续再通过workers查询数据库来实现接口的返回即可。
这个工作流就又可以定时执行了,感觉每个月执行一次都可以,如果想要更新及时,那么间隔调短一点吧(一般也不会变,每年都是国家提前定好的,好像也没见变过)。不过这个及时性,其实也依赖于仓库作者的更新及时性,我们用来做学习,完全没啥问题。