1 前言
原本没想写这篇文章,之所以忽然插队来写,是因为我忽然对”纯手动定期更换博客的背景图片”这种行为有点倦怠了~。
我博客的背景图片一直是我定期手动更换的(图片存放在cloudflare R2上)。本来嘛,1-2个月更换一次背景图片倒也不算麻烦,但是随着用过的背景图片越来越多,之前用过的完全弃之不用我也有点舍不得(毕竟是我精挑细选出来的),但是让我主动的手动更换成以前用过的背景图片,主观上我又有点不情愿。
咋办呢?要不干脆搞个随机背景,这样一来就像皇帝侍寝翻牌子,翻到那个就是哪个,我也不用纠结了~。想到就开始做,刚好也可以水一篇文章~。
2 第二部分:实现方式的选择
在搭建随机图片 API 时,可以根据”是否需要 VPS”作为判断条件来选择适合自己实际条件的方案:
1. 需要 VPS 的方案(适合有 VPS 或使用 Cloudflare Tunnel 的朋友)
如果你已经有 VPS(或者采用 Cloudflare Tunnel 建站),可以选择以下两种方式之一:
• 本地存储:图片直接存放在 VPS 上,服务器通过 API 随机返回一张图片。适合小规模图片库,访问速度快,但占用 VPS 存储空间。
• 云存储:图片存放在 Cloudflare R2、S3、OSS 等云端存储,VPS 负责 API 逻辑。适合大规模图片库,减少 VPS 负担,但访问依赖外部存储。
2. 不需要 VPS 的方案(Cloudflare Worker + 云存储)
如果你没有 VPS,或者希望完全托管在 Cloudflare,可以选择:
• Cloudflare Worker + 云存储(R2 / GitHub):Cloudflare Worker 处理 API 请求,图片存放在 Cloudflare R2、GitHub 仓库等位置,直接返回图片 URL。无需服务器,维护成本低,适合托管型服务。
注:除了Cloudflare Worker,还有其他选择,只是我本身就在使用Cloudflare,所以就顺理成章的选择Worker方案了,并且,在着眼于全球访问这个角度,Worker能运行在Cloudflare遍布全球的边缘网络上也是极大的优势。
3 具体实现步骤
3.1 需要VPS的方案
3.1.1 本地存储方式
这种方式应该是最容易实现的随机图片 API 方案了:只需要在 VPS 上安装 Nginx 或 Apache作为WEB服务器软件并进行简单的设置即可实现(注意,如果选择Nginx,需要确保已经正确配置 PHP 解析(docker方式部署Nginx + PHP环境可以参考文章:docker系列 单容器nginx、单容器php(一个版本)之多站点共用;宝塔面板部署Nginx + PHP环境可以参考文章:linux面板系列 基于宝塔面板以源码方式部署V免签);如果选择Apache,需确认”mod_php”模块已安装):
3.1.1.1 普通版 (不区分 PC 和移动端)
新建一个 WEB 站点,在站点的根目录下创建一个”pc_img”目录(放入准备好的壁纸图片),以及一个 index.php 文件,其内容如下:
基于以上即可实现一个简单的 随机图片API(index.php 代码的作用是随机选择一张图片并返回)。最后的目录结构如下:
/var/www/html/
│── pc_img/ # 存放壁纸图片
│── index.php # API 代码
假设站点对应的域名为”image.tangwudi.com“,那么,访问”https://image.tangwudi.com
“,将随机返回 “pc_img” 目录下的图片。
3.1.1.2 进阶版 (兼容 Mobile-Detect,区分 PC 和移动端)
这种方式要更先进一点,主要是依靠Github上的一个项目:https://github.com/serbanghita/Mobile-Detect,该项目是一个 PHP 设备检测库,主要用于 判断访问者的设备类型(如 手机、平板、PC)。它基于 User-Agent 解析,可用于 移动端适配、动态内容调整、重定向 等场景,其可以帮助开发者 精准区分 PC、手机和平板,非常适合 响应式设计、内容优化、API 适配 等场景,就比如本文中的这种用法。
依旧需要新建一个 WEB 站点,不过这次在站点的根目录下除了创建一个”pc_img”目录(放入用于pc访问所看到的壁纸图片),还需要创建一个”mobile_img”目录(放入用于移动端访问所看到的壁纸图片),一个Mobile_Detect.php文件,该文件可以使用以下命令下载:
wget https://raw.githubusercontent.com/serbanghita/Mobile-Detect/master/Mobile_Detect.php -O Mobile_Detect.php
一个 index.php 文件,内容如下:
isMobile() && !MobileDetect->isTablet();
// 选择文件夹(PC / 移动端不同壁纸)folder = is_mobile ? "mobile_img" : "pc_img";
// 获取本地对应文件夹中的图片img_array = glob(__DIR__ . "/folder/*.{webp,gif,jpg,png}", GLOB_BRACE);
// 如果目录中没有图片,返回 404
if (!img_array) {
http_response_code(404);
die("No images found");
}
// 随机选择一张图片
random_img =img_array[array_rand(img_array)];
// 302 重定向到该图片
header("Location: " . str_replace(__DIR__, '',random_img));
exit;
?>
最后的目录结构如下:
/var/www/html/
│── pc_img/ # 存放 PC 端壁纸
│── mobile_img/ # 存放移动端壁纸
│── Mobile_Detect.php # 设备检测库
│── index.php # API 代码
进阶版 的访问方式与普通版 相同,但会根据设备类型自动选择 pc_img/ 或 mobile_img/ 目录中的图片。
3.1.2 云存储方式
3.1.2.1 概述
这种方式适合希望利用云存储(如 Cloudflare R2、AWS S3、Github仓库等)存放图片,同时仍然使用 VPS 作为 API 逻辑处理层的朋友。相比本地存储方案,这种方式减少了 VPS 的存储压力,并能利用云存储的高可用性和 CDN 加速能力。
3.1.2.2 普通版 (不区分 PC 和移动端)
实现步骤:
- 在云存储(Cloudflare R2 / S3 /Github)中存放图片,并确保它们可以通过 HTTP 访问。
- 在 VPS 上创建 pc_img.txt 文件,存储图片的链接,每行一张图片的 URL。
- VPS 端 PHP 读取 pc_img.txt 并随机返回一张图片的 URL。
- (可选)使用脚本定期更新 pc_img.txt,确保云存储中的新图片能被 API 访问。
步骤 1:在云存储中存放图片
以 Cloudflare R2 为例,假设你的 R2 公开存储地址为”https://image.tangwudi.com/
“(需提前配置R2、创建存储桶等初始化操作,可以参考文章:家庭数据中心系列 cloudflare教程(八) CF R2功能介绍及基于R2搭建图床的详细教程),且每张图片的访问地址如下:
https://image.tangwudi.com/img1.jpg
https://image.tangwudi.com/img2.png
https://image.tangwudi.com/img3.webp
步骤 2:在 VPS 上创建 pc_img.txt
在 pc_img.txt 中,每行存放一张图片的 URL:
https://image.tangwudi.com/img1.jpg
https://image.tangwudi.com/img2.png
https://image.tangwudi.com/img3.webp
步骤 3:VPS 端 index.php 代码
代码逻辑:
• 读取 pc_img.txt 中的图片列表。
• 如果 pc_img.txt 为空,则返回 404。
• 随机选择一张图片,并 302 重定向 到该图片的 URL。
访问 “https://image.tangwudi.com/
“,将随机返回 pc_img.txt 里的一张图片 URL
目录结构:
/var/www/html/
│── pc_img.txt # 存放云存储的图片 URL
│── index.php # API 代码
步骤 4:定期更新 pc_img.txt(可选)
如果你的云存储图片会频繁更新,可以用定时任务 自动同步 img.txt。
Linux 定时任务示例(适用于 AWS S3)
在 VPS 上运行以下命令,把存储桶的图片 URL 自动写入 img.txt:
aws s3 ls s3://your-bucket-name/ --recursive | awk '{print "https://image.tangwudi.com/"$NF}' > /var/www/html/pc_img.txt
然后在 crontab -e 中添加定时任务,每小时更新一次:
0 * * * * /path/to/script.sh
如果你用的是 Cloudflare R2,可以写个 Python 脚本定期调用 r2 API 获取图片列表,然后更新 img.txt。
3.1.2.3 进阶版 (兼容 Mobile-Detect,区分 PC 和移动端)
进阶版 相对于普通版 ,要多一个mobile_img.txt,专门用来存放适合移动设备壁纸图片的URL,同时和本地存储方式的进阶版 一样,也要多一个Mobile_Detect.php,而index.php的内容也有所不同:
isMobile() && !MobileDetect->isTablet();
// 选择文件(PC / 移动端不同壁纸)filename = is_mobile ? "mobile_img.txt" : "pc_img.txt";
// 文件不存在,返回 404
if (!file_exists(filename)) {
http_response_code(404);
die("File not found");
}
// 读取图片链接(跳过空行和注释)
pics = array_filter(file(filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES), function(line) {
return substr(line, 0, 1) !== '#';
});
// 如果图片列表为空,返回 404
if (empty(pics)) {
http_response_code(404);
die("No images found");
}
// 随机选择一张图片random_img = pics[array_rand(pics)];
// 302 重定向到该图片
header("Location: " . $random_img);
exit;
?>
目录结构如下:
/var/www/html/
│── pc_img.txt # 存放 PC 端壁纸 URL
│── mobile_img.txt # 存放移动端壁纸 URL
│── Mobile_Detect.php # 设备检测库
│── index.php # API 代码
进阶版 的访问方式与普通版 相同,但会根据设备类型自动选择”pc_img.txt” 或 “mobile_img.txt” 里的图片 URL。
使用 VPS + PHP 来实现随机图片 API,需要依赖 index.php 来处理每次请求:每当用户请求图片时,请求必须回源到 VPS,这会带来以下几个问题:
1. VPS 连通性要求高
• 回源依赖:每次请求都必须由 VPS 回源去获取图片,若 VPS 出现问题(比如网络故障、服务器压力过大等),就会导致请求失败或延迟,另外,谁能保证所有用户都能快速访问到VPS?
• 带宽瓶颈:如果访问量大且图片较大,VPS 的带宽会成为瓶颈,尤其是采用本地存储方式且CDN又没有合理配置得时候。
2. 高并发情况下性能压力大
• 每次请求都会由 VPS 承载,这在流量大时容器导致 VPS 性能过载。
• 在访问量突然暴增时,VPS 的硬件和带宽可能不够用,导致 响应超时 或 崩溃。
3. 缓存问题
• 即使使用 CDN 缓存了图片,但是每次的 随机图片选择 还是需要通过 PHP 后端来处理,CDN 只会缓存最终返回的图片 URL,而不会缓存 index.php 的判断逻辑。
• 每次图片变动时,缓存刷新 需要通过回源到 VPS 进行管理,这样一来对于大规模的更新或高并发请求时,会造成缓存命中率较低,影响性能。
4. VPS 成本
• VPS 在高并发情况下,如果需要更多的带宽、处理器和内存,就会面临成本增加。
所以,对于访问量不大且对高可用性要求不高的使用场景(比如个人博客的背景图),基于VPS的随机图片API方案到还行。但是一旦访问量大了或者对高可用的要求高,亦或者对响应速度有要求,这种方案就不合适了。
注1:以上基于VPS的方案,除了需要熟悉常规建站方式以外,还需要熟悉反向代理的配置(公网IP方式建站)或者有Cloudflare账号并能完成常规功能的配置(Cloudflare tunnel方式建站)。不熟悉反向代理的配置的朋友,可以参考文章:linux面板系列 配置反向代理并使用非443端口进行发布(使用宝塔面板实现反向代理)、docker系列 使用docker基于NPM搭建自己的反向代理(使用NPM实现反向代理);不熟悉Cloudflare tunnel配置的朋友,可以参考文章:家庭数据中心系列 通过tunnel技术,让无公网IP的家庭宽带也能白嫖cloudflare实现快速建站(推荐)。
注2:以上这部分内容是我看了网上常规的基于VPS的随机图片API教程后,习惯性的梳理总结一下,并非本文的重点,我也并不推荐,所以一笔带过。
3.2 不需要VPS的方案(Cloudflare Worker + R2)
3.2.1 概述
这种方案其实是之前”VPS+云存储”方案的改良版,虽说是不需要VPS,但是却需要有技术能代替”VPS+云存储”方案中”index.php”的作用(随机返回一张图片的URL)。我的图床本来就是用Cloudflare R2来搭建的,所以使用Cloudlfare Worker来实现”index.php”的功能就很合理了:


文章前面提到基于VPS + PHP来实现随机图片API的方案会面临的一些问题,而基于Cloudflare Worker 的方案就没有这些问题,反而在这些方面有很多优势:
1. 去中心化,无需回源
• Worker 完全运行在 边缘节点,能够直接响应请求而不需要依赖远程 VPS。
• 图片存储在 Cloudflare R2 或其他云存储服务,Worker 会直接从这些地方获取图片并返回,不需要回源到 VPS。
2. 高并发下的可伸缩性
• Worker 是 无状态 的,不依赖服务器硬件资源,Cloudflare 的边缘网络自动进行 负载均衡,即使请求量暴增,也不会影响性能。
• Worker 按 请求量计费,大部分情况下比 VPS 更具性价比,特别是对于流量大而计算量小的应用场景(每天10万请求的免费额度,只要不遇到DDoS攻击,常规的个人博客绝对用不完~)。
3. 优秀的缓存机制
• Cloudflare 本身提供 强大的缓存机制,可以在 CDN 节点层缓存图片和返回结果,减少 Worker 的计算压力和回源需求。
• 图片可以被缓存更长时间,而不需要每次都请求回源,大幅度提高性能。
4. 灵活的图片存储与优化
• 图片可以存放在 R2 或其他云存储,并且利用 Cloudflare 的 Polish 和 Mirage 功能 进行自动优化和压缩(需要订阅pro用户),不用自己手动处理。
• Worker 可以配合缓存策略进行智能缓存和图片更新,减少重复请求。
5. 低成本、高可用
• Worker 的计费模式是按 请求量和资源消耗 计费,适合流量不规律的应用场景。
• 不需要 高性能 VPS 或负载均衡器 来处理大流量,降低了运维成本。
得出结论:免费还更强,还要啥自行车?
不过,和”VPS + 云存储”场景不同的是,R2上的文件不能直接编辑,如果依然按照”VPS + 云存储”场景中的img.txt的思路就会很麻烦:每次新增图片就需要重新编辑并上传pc_img.txt文件(v2.0版 还要编辑并上传mobile_img.txt文件),虽然说可以通过rclone或者alist之类的方式让这个编辑上传的行为变得不那么麻烦,但是对于我这种懒人来说,还是太折磨了,所以,需要改变一下思路:
- 考虑到Worker方式的功能都靠代码来完成,检测终端类型不用像VPS方式那样还要单独使用一个”Mobile_Detect.php”文件来实现,所以,”Cloudflare Worker + R2″这种方案下的v1.0版 就应该包括终端检测的功能(相应的,v1.0版 对应的的图片文件夹也就需要2个)
- 理想的管理运维效果就是采用文件夹管理的方式:在R2上创建目录”pc_img”和”mobile_img”,只要把适合PC端和移动端的壁纸分别上传到2个目录中,然后Worker就自动基于访问终端设备类型在2个目录中随机选择图片之后,把图片URL返回给访客即可,不再需要额外的操作(txt文件之类~)。
其实,在文章前面介绍的VPS + 本地存储方式中,也是采用的基于文件夹的管理运维方式,不过,那是因为PHP可以直接获取pc_img和mobile_img目录下的文件内容(通过本地文件系统),而Cloudflare Worker可以直接获取R2存储桶里的文件内容(需要在Worker”设置”中绑定R2存储桶),所以才能采用这种方式。
注1:如果云存储不是R2(比如是Github仓库),那么,想要用这种”文件夹方式”来管理运维就要麻烦不少,因为Worker要获取Github仓库中的图片内容只能通过API调用的方式来实现,虽然看起来只是多了一步,但是代码的复杂度、代码的调试都会增加不少难度,远不及”Worker + R2″来得方便。当然,也可以退一步用txt文件的方式,就是平时需要多费点手脚同时逼格略为low了一点~。
注2:Cloudflare Worker 是 Cloudflare 提供的一种无服务器计算(Serverless)平台,允许开发者在 Cloudflare 的全球边缘网络上运行 JavaScript 代码。它基于 V8 引擎,支持快速处理 HTTP 请求,无需管理服务器,适用于 API 代理、内容重写、静态资源分发等场景。由于 Worker 运行在 Cloudflare 的边缘节点,能提供低延迟、高可用的服务,并且可以结合 KV 存储、Durable Objects 等功能扩展应用能力,更多关于Cloudflare Worker的知识和配置细节可以参看文章:家庭数据中心系列 cloudflare教程(七) CF worker功能介绍及基于worker实现”乞丐版APO for WordPress”功能加速网站访问的实操、验证及相关技术原理研究。
3.2.2 准备工作
3.2.2.1 在R2上创建专门用于随机壁纸的存储桶(可选但是强烈建议)
其实,我个人是建议是专门建一个用于随机壁纸的存储桶,哪怕是之前已经把R2作为图床使用的、有现成存储桶的朋友。其实到不是非要这样做,只是除了新建Worker的时候需要设置基于域名的路由(一般就使用存储桶的自定义域名,新的存储桶+新的域名设置比较方便),Worker本身要正常工作,就需要页面规则、缓存规则、WAF自定义规则等在流量序列中优先级高于Worker的规则的正确配置(Cloudflare流量序列的相关概念请参考文章:家庭数据中心系列 cloudflare教程(二) CF整体方案流量序列中各技术节点功能简介),如果直接利用已有的存储桶,我担心对Cloudflare流量优先级以及这些规则配置不是很熟悉的朋友会有所疏忽,导致随机图片效果无法实现,所以最好的方法,是新建一个专门用于随机壁纸功能的存储桶,并使用新的三级域名,这样可以很大程度上回避之前规则配置的影响,比如我就新建了一个名为wallpaper的存储桶。
按照如下图片教程操作:



选择”设置”选项卡:

配置自定义域名:


随后点击”连接域”即可在DNS中生成一条类型为R2的记录:

3.2.2.2 创建KV
注:如果不需要后续v2.0版 和v3.0版 的速率限制功能(防恶意刷流),这一步可以跳过。


完成:

之所以选KV而没选用D1数据库来存放,是因为如果仅仅只是临时存放IP访问的计数(60秒一过就清零重新开始),KV就足够且速度也快,如果有更复杂的需求,比如记录IP的访问次数还提供查询功能等,就需要D1数据库了。
3.2.2.3 上传壁纸图片到R2的wallpaper存储桶
• PC 端壁纸存放在 pc_img/ 目录
• 移动端壁纸存放在 mobile_img/ 目录
示例目录结构如下**:
pc_img/
│── pc1.jpg
│── pc2.png
│── pc3.webp
mobile_img/
│── mobile1.jpg
│── mobile2.png
│── mobile3.webp
注:上面的图片名字只是示范而已,实际上名字随意即可,不影响效果。
3.2.3 v1.0版 worker代码的实现
3.2.3.1 创建worker



3.2.3.2 编辑代码

将以下v1.0版 代码复制并粘贴到worker并进行部署:
export default {
async fetch(request, env, ctx) {
const bucket = env.WALLPAPER_BUCKET; // 绑定 Cloudflare R2 存储桶
const url = new URL(request.url);
const typeParam = url.searchParams.get("type"); // 获取 URL 参数 ?type=pc 或 ?type=mobile
const userAgent = request.headers.get('User-Agent') || ''; // 获取访问者的 User-Agent 信息
// 判断是否是 PC 设备
const isPC = /Windows NT|Macintosh|X11|Linux/i.test(userAgent);
// 判断是否是移动设备
const isMobile = /Android|iPhone|iPad|iPod|webOS|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
// 默认从 pc_img 文件夹获取壁纸
let folder = "pc_img/";
// 如果 URL 参数指定了 type=mobile,则使用 mobile_img
if (typeParam === "mobile") folder = "mobile_img/";
// 如果 URL 参数指定了 type=pc,则强制使用 pc_img
else if (typeParam === "pc") folder = "pc_img/";
// 如果没有 URL 参数,则根据 User-Agent 判断设备类型
else if (!isPC && isMobile) folder = "mobile_img/";
try {
// 获取 R2 存储桶中对应文件夹的图片列表
const objects = await bucket.list({ prefix: folder });
const items = objects.objects;
// 如果该目录下没有图片,返回 404
if (items.length === 0) return new Response('No images found', { status: 404 });
// 随机选择一张图片
const randomItem = items[Math.floor(Math.random() * items.length)];
// 生成图片的 URL
const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`;
// 302 重定向到图片 URL
return Response.redirect(imageUrl, 302);
} catch (error) {
console.error('Error:', error); // 记录错误信息
return new Response('Internal Server Error', { status: 500 }); // 发生错误时返回 500
}
}
};
如下图:

这段代码的主要功能是”根据访问设备的类型(PC 或移动端)从 Cloudflare R2 存储桶中随机返回一张图片URL“,它的工作流程如下:
1️. 识别用户设备
• 代码会先检查访问者的 User-Agent(即浏览器的身份信息)。
• 如果是 手机或平板(iPhone、Android 等),就从 mobile_img/ 文件夹选取壁纸。
• 否则,就默认从 pc_img/ 文件夹选取壁纸。
2️. 获取文件列表
• 代码向 Cloudflare R2 存储桶 发送请求,获取对应文件夹下的所有图片列表。
• 如果发现文件夹里没有图片,就返回 404 Not Found(说明当前类别下无图片可用)。
3️. 随机选择一张图片
• 代码会从图片列表中随机挑选一张。
• 然后尝试从 R2 存储桶中获取这张图片的具体内容。
• 如果这张图片无法找到,返回 404 Not Found(可能是文件丢失)。
4️. 设置正确的图片格式
• 代码会检查 R2 存储桶是否记录了这张图片的 Content-Type(比如 image/png 或 image/jpeg)。
• 如果没有找到,就默认将其当作 image/jpeg 处理,以确保浏览器能正确加载。
5️. 返回图片 🚀
• 代码最终把图片的URL返回给用户,让用户可以正常看到壁纸。
3.2.3.3 绑定R2存储桶


变量名称按如下填写,注意不要写错:
WALLPAPER_BUCKET
如下图:

3.2.3.4 添加入口路由



最终完成后:

3.2.3.5 最终效果
在保证以上每一步配置都正确无误,且页面规则、缓存规则和WAF规则没出错的情况下(如果存储桶的自定义域名是新建的域名,一般没问题),访问请求可以命中worker,则正常效果是:
• 访问 https://wallpaper.tangwudi.com
,根据访问终端类型,返回一个 对应的随机壁纸的 URL。
• 访问 https://wallpaper.tangwudi.com?type=mobile
,专门返回 移动端壁纸 URL。
• 访问 https://wallpaper.tangwudi.com?type=pc
,强制返回 PC 端壁纸 URL。
测试成功后开心了一会,但是细细一琢磨,感觉这段代码只是实现了最基本的随机图片API功能,虽然说对一般朋友来说可能够了,但是,如果真要深入探究,会发现问题不少,感觉只是半成品而已。
3.2.3.6 问题罗列
1、未考虑屏蔽爬虫
虽然说Cloudflare WAF里可以使用自定义规则来过滤爬虫,但是我相信大部分朋友可能都不熟悉如何配置(虽然我强烈建议大家好好研究下Cloudflare WAF的自定义规则,这是免费且强大的功能),所以,最好是在代码里就能简单的防护。
2、未考虑速率限制
在文章前面添加路由的时候,有个选项叫”失败次数”,实际上就是worker每日免费使用次数超过10万次时候的处理行为,有”失败时自动关闭(阻止)”和”失败时自动打开(继续)”。在前面文章的截图中我提了一句:对于v1.0版 来说,选任意一种行为都没差别,为什么这么说呢?
因为对于一般的网站而言,”失败时自动打开(继续)”相当于直接回源,是可以访问到数据的,只是可能因为没有缓存速度会慢一些。但是对于随机图片API而言,”https://wallpaper.tangwudi.com
“的响应全靠worker来实现,一旦超限,worker直接不工作了,这时候直接回源,R2也不会有回应,所以选哪个选项都没有意义。
注1:关于超限的问题,可能有的朋友会说:”怎么可能超限,每日10万次请求的额度,一般日访问量1000的比较牛逼的个人博客,也才1000而已,怎么都用不完”。一般来说是这样没错,但是那是没有遇到被恶意刷流量的情况,我的图床之前没有做什么访问限制,结果某天凌晨2小时之内被刷了5T流量,逼得我直接关闭了图床的直接访问。说了这么多,其实就是在代码里需要考虑速率限制:如果单位时间内某个IP访问次数超过阀值就不再响应。
注2:v2.0版 代码的速率限制,坦白讲,只能防备一些恶意刷流访问,但是对上真资格的DDoS攻击,死得也不过晚那么一秒钟~,所以还是要合理配置Cloudflare的DDoS攻击防护相关的功能。
3、未考虑缓存优化
虽然每次刷新或者切换页面都更换背景图片感觉蛮酷,但是从站长的角度来考虑却未必是件好事:每次刷新就意味着worker会处理一次请求,浏览器短时间内频繁访问 Worker,会增加不必要的资源消耗,且从用户的体验来说,每次都有一个背景图片加载的过程也未必就一定爽,所以最好能在代码里增加适度的浏览器本地缓存(其实这个也可以通过页面规则或者缓存规则来实现,不过和WAF自定义规则一样的道理,我相信大部分朋友都未必会去设置)。
4、基于查询参数的API调用引发的worker路由设置太粗旷
v1.0版 提供的API选项是”?type=pc”和”?type=mobile”,不过,由于worker的入口路由不能带查询参数,所以要想支持”?type=”的选项,其路由就只能用”wallpaper.tangwudi.com/* “,但是这样一来,worker就不得不处理所有路径的请求(包括恶意的请求)。
注:这里当然可以用WAF的自定义规则进行拦截,不过还是那句话,我相信大部分朋友都未必会去设置,所以最好是在worker代码层面就能够实现一定程度的预防。
5、其他问题
另外,v1.0版 脚本较为简陋,未包含错误日志和调试支持,从代码的角度来说也不算完整。
基于以上问题,诞生了v2.0版 。
3.2.4 v2.0版 worker代码的实现
在worker中用以下v2.0版 代码替换之前的v1.0版 代码并进行部署:
export default {
async fetch(request, env, ctx) {
const bucket = env.WALLPAPER_BUCKET; // 绑定 Cloudflare R2 存储桶
const url = new URL(request.url);
const pathname = url.pathname.toLowerCase(); // 获取请求路径(忽略大小写)
// **屏蔽常见爬虫工具**
const userAgent = (request.headers.get("User-Agent") || "").toLowerCase();
const cfRay = request.headers.get("cf-ray") || "unknown";
const botPatterns = [
"curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime",
"go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib",
"apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp",
"axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs"
];
if (!userAgent.trim()) {
console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`);
return new Response("Forbidden: Empty User-Agent", { status: 403 });
}
if (botPatterns.some(bot => userAgent.includes(bot))) {
console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: {cfRay}`);
return new Response("Forbidden: Bot Detected", { status: 403 });
}
// **确定文件夹(PC / Mobile)**
let folder = "pc_img/"; // 默认 PC
if (pathname.endsWith("fallback_mobile.jpg")) {
folder = "mobile_img/"; // 强制移动端壁纸
} else if (pathname.endsWith("fallback_pc.jpg")) {
folder = "pc_img/"; // 强制 PC 端壁纸
} else {
// **`fallback.jpg` 需要根据 User-Agent 识别**
const isMobile = /iphone|ipod|android|blackberry|iemobile|opera mini/.test(userAgent);
folder = isMobile ? "mobile_img/" : "pc_img/";
}
console.log("Requested Path:", pathname);
console.log("Selected Folder:", folder);
// **初始化内存缓存(Map)**
const rateLimitCache = new Map(); // 用于缓存速率限制数据
// **速率限制(60 秒内最多 10 次请求)**
const clientIP = request.headers.get("CF-Connecting-IP") || "unknown";
const rateLimitKey = `rate-limit-{clientIP}`;
// 尝试从内存缓存获取速率限制数据
let currentCount = rateLimitCache.get(rateLimitKey);
if (!currentCount) {
// 如果缓存中没有,尝试从 KV 存储获取
try {
currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey);
if (currentCount) {
currentCount = parseInt(currentCount);
} else {
currentCount = 0;
}
} catch (error) {
console.error("Error fetching rate limit from KV:", error);
// 如果 KV 读取失败,默认从 0 开始
currentCount = 0;
}
}
const newCount = currentCount + 1;
// 如果超出速率限制,返回 429 错误
if (newCount > 10) {
return new Response("Too Many Requests", { status: 429 });
}
// 更新内存缓存和 KV 存储
rateLimitCache.set(rateLimitKey, newCount);
try {
// 如果 KV 存储存在,则更新 KV 存储中的数据
await env.RATE_LIMIT_KV.put(rateLimitKey, newCount, { expirationTtl: 60 });
} catch (error) {
console.error("Error updating rate limit in KV:", error);
// 即使 KV 更新失败,也不影响请求继续处理
}
try {
// **获取 R2 存储桶中的文件列表**
const objects = await bucket.list({ prefix: folder });
const items = objects.objects;
if (items.length === 0) {
return new Response("No images found", { status: 404 });
}
// **随机选择一张图片**
const randomItem = items[Math.floor(Math.random() * items.length)];
if (!randomItem) {
return new Response("No valid images", { status: 404 });
}
const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`;
// **设置缓存策略**
const headers = new Headers();
headers.set("Cache-Control", "public, max-age=600"); // 浏览器缓存 10 分钟
headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN 缓存 7 天
headers.set("ETag", randomItem.etag);
// **302 重定向到图片 URL**
return Response.redirect(imageUrl, 302);
} catch (error) {
console.error("Error in Worker:", error);
return new Response("Internal Server Error", { status: 500 });
}
}
};
然后需要绑定之前创建的KV空间(需要使用KV来存放IP的访问次数的统计内容):

变量名称按如下填写,注意不要写错了:
RATE_LIMIT_KV
如下图:

最终完成后绑定的资源除了R2存储桶,还有KV命名空间:

这段v2.0版 代码与上面的v1.0版 代码相比,做了许多重要的改进和增强:
1. 爬虫过滤 (Bot Detection)
v1.0版 代码没有做爬虫限制,只是直接处理请求,可能会遭遇大量爬虫流量或者恶意刷流的行为,v2.0版 代码可以进行一定程度的防护:
// **屏蔽常见爬虫工具**
const userAgent = (request.headers.get("User-Agent") || "").toLowerCase();
const cfRay = request.headers.get("cf-ray") || "unknown";
const botPatterns = [
"curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime",
"go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib",
"apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp",
"axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs"
];
if (!userAgent.trim()) {
console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`);
return new Response("Forbidden: Empty User-Agent", { status: 403 });
}
if (botPatterns.some(bot => userAgent.includes(bot))) {
console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: ${cfRay}`);
return new Response("Forbidden: Bot Detected", { status: 403 });
}
• 问题解决:通过匹配 User-Agent 字段来判断是否是常见爬虫工具,若是爬虫则返回 403 禁止访问,这能有效防止不受欢迎的自动化访问。
2. 速率限制 (Rate Limiting)
v1.0版 代码中,任何IP的请求都不受限制,可能会遭遇滥用请求,v2.0版 代码多了一定的防护能力:
// **初始化内存缓存(Map)**
const rateLimitCache = new Map(); // 用于缓存速率限制数据
// **速率限制(60 秒内最多 10 次请求)**
const clientIP = request.headers.get("CF-Connecting-IP") || "unknown";
const rateLimitKey = `rate-limit-${clientIP}`;
// 尝试从内存缓存获取速率限制数据
let currentCount = rateLimitCache.get(rateLimitKey);
if (!currentCount) {
// 如果缓存中没有,尝试从 KV 存储获取
try {
currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey);
if (currentCount) {
currentCount = parseInt(currentCount);
} else {
currentCount = 0;
}
} catch (error) {
console.error("Error fetching rate limit from KV:", error);
// 如果 KV 读取失败,默认从 0 开始
currentCount = 0;
}
}
const newCount = currentCount + 1;
// 如果超出速率限制,返回 429 错误
if (newCount > 10) {
return new Response("Too Many Requests", { status: 429 });
}
// 更新内存缓存和 KV 存储
rateLimitCache.set(rateLimitKey, newCount);
try {
// 如果 KV 存储存在,则更新 KV 存储中的数据
await env.RATE_LIMIT_KV.put(rateLimitKey, newCount, { expirationTtl: 60 });
} catch (error) {
console.error("Error updating rate limit in KV:", error);
// 即使 KV 更新失败,也不影响请求继续处理
}
• 问题解决:通过引入 KV 存储和速率限制机制来限制同一 IP 在 60 秒内只能请求 10 次。超过此次数,则返回 429 状态码(请求过多),有效防止恶意请求和滥用,等到过了60秒统计清零之后又正常响应。
• 增强功能:速率限制为每个 IP 单独计算,限制了访问频率,防止同一个 IP 发起过多请求,一定程度上能防护部分恶意刷流行为。
3. 缓存控制
v1.0版 代码没有在返回响应时设置 ETag 和 Cache-Control 头部,缓存策略完全依赖于 Cloudflare 或浏览器的默认行为,v2.0版 代码对缓存进行了优化:
// **设置缓存策略**
const headers = new Headers();
headers.set("Cache-Control", "public, max-age=600"); // 浏览器缓存 10 分钟
headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN 缓存 7 天
headers.set("ETag", randomItem.etag);
• 问题解决:设置了 Cache-Control 头部,指定响应可以被浏览器缓存 10 分钟,这帮助减少了服务器负担,并且配合 ETag 实现了更精确的缓存控制;同时通过设置CDN-Cache-Control,让图片能够在Cloudflare的边缘缓存上存在7天。
• 增强功能:虽然 Cache-Control 可以减少访问频率,但也允许在一定时间内更新背景图。结合 ETag和边缘缓存,用户浏览器和 Cloudflare 会更智能地处理缓存。
不过,相比在worker中配置缓存,我真心建议还是使用Cloudflare的缓存规则(或者页面规则,但是页面规则来实现缓存有点浪费,没啥必要)来实现,简单明了,如下图:

如果同时开启了Cloudflare的图像优化功能,比如Polish、Mirage,则可能导致本地浏览器缓存10分钟的策略失效:
• Polish 和 Mirage 都有可能改变图片的内容或格式,这可能导致浏览器和 CDN(Cloudflare)缓存不命中,进而导致每次加载新图片。
• Polish 可能导致不同格式的图片(如 WebP 和原始格式),并且每次请求时可能不同,这会导致缓存未命中。
• Mirage 则可能因为设备和网络条件的不同,返回不同版本的图片,导致缓存不一致。
4、去掉代码基于查询参数”?type=”的判断,改为静态路径的判断
从安全角度考虑,使用静态路径(比如/fallback_pc.jpg
和 /fallback_mobile.jpg
)替代查询参数,可以使 Worker 路由更加简洁(wallback.tangwudi.com/*
太过于粗旷了):只需要这三条具体的路由即可明确区分设备类型,避免了对查询参数的依赖:

同时,对随机图片API的访问、强制返回PC壁纸及强制返回移动端壁纸的方式变化如下:
• 访问 https://wallpaper.tangwudi.com/fallback.jpg
,根据访问终端类型,返回一个“对应的随机壁纸的 URL”(这么改的原因见后)。
• 访问 https://wallpaper.tangwudi.com/fallback_mobile.jpg
,强制返回”移动端壁纸 URL”。
• 访问 https://wallpaper.tangwudi.comfallback_pc.jpg
,强制返回”PC 端壁纸 URL”。
这样不仅减少了恶意用户通过伪造查询参数绕过设备识别的风险,还能有效避免对不合理或恶意随机路径的处理,从而提升整体的安全性和系统稳定性。
虽然添加了速率限制功能以后可以一定程度上缓解worker遇到恶意刷流行为导致的使用超限的问题,但是,天要下雨,娘要嫁人,遇到了之后真要超限也没办法,”worker超限后背景图片无法打开”的问题终究还是需要面对的。
其实有个最简单的办法,不需要改动v2.0版 代码即可实现:在wallpaper存储桶的”根路径”(加引号是因为对COS来说,”根路径”、”目录”这些传统文件系统的概念其实都只是人类的幻觉而已)下放一张图片,假设名为fallback.jpg(对应前面的访问),如下图:

那么,只需要将背景壁纸图片的链接设置为”https://wallpaper.tangwudi.com/fallback.jpg
“即可(刚好和worker的路由对应),比如我的argon主题的页面背景设置:

为什么这样设置就能解决呢?因为前面对worker的入口路由是设置的wallpaper.tangwudi.com/fallback.jpg
,所以设置这个地址可以被worker处理,而一旦worker使用次数超限而罢工,而恰好R2的wallpaper存储桶的失败模式又是设置的”失败时自动打开(继续)”:

那么对背景图片的访问就从之前worker返回的图片URL变成了直接访问如下链接:”
https://wallpaper.tangwudi.com/fallback.jpg
“,而这个图片地址是真实存在的,所以就实现了worker使用超限之后的背景图片冗余了。
注:需要注意的是,虽然实现了worker使用超限后的背景图片的容灾,但是如果是因为遭遇DDoS的攻击而导致的worker使用超限,那么之后的攻击流量就会直接打到R2上,所以关键还是要做好Cloudflare上DDoS防护相关的设置,不熟悉的朋友可以参看文章:家庭数据中心系列 cloudflare教程(五) DDoS攻击介绍及CF DDoS防护配置教程。
5. 更好的错误处理,增强的日志输出
v1.0版 代码处理错误时仅做了简单的日志记录和返回 500 错误,终极版代码的错误处理更加全面和详细:
console.error(' Error in Worker:', error); // 记录详细错误日志
return new Response('Internal Server Error', { status: 500 });
• 问题解决:v2.0版 中不仅捕获了错误,还将错误详细记录到日志中,便于调试和排查问题。虽然依然返回 500 错误,但是有了更详细的错误输出。
v1.0版 代码中没有什么日志输出,调试时可能不容易获得访问的详细信息,v2.0版 代码中加入了日志输出:
console.log("User-Agent: ", userAgent); // 记录日志,方便调试
console.log("Selected Folder: ", folder); // 记录日志,方便调试
• 问题解决:加入了对 User-Agent 和选择的文件夹的日志记录,方便开发者跟踪和调试请求和文件选择逻辑。
3.2.5 v3.0版 worker代码的实现(效果待验证,暂不推荐)
v2.0版 对正常的个人博主而言完全够了,但是,关于”worker免费请求次数超限后,访问https://wallpaper.tangwudi.com/fallback.jpg
会直接访问到R2上”这个问题我还是比较在意的:如果是遭受到DDoS攻击而引发的worker使用次数超限,攻击就会直接打到R2上,总感觉不妥当。所以,最好能进一步优化代码来避免这种情况,基于这个考虑v3.0版 代码诞生:
export default {
async fetch(request, env, ctx) {
const bucket = env.WALLPAPER_BUCKET; // 绑定 Cloudflare R2 存储桶
const url = new URL(request.url);
const pathname = url.pathname.toLowerCase(); // 获取请求路径(忽略大小写)
// **屏蔽常见爬虫工具**
const userAgent = (request.headers.get("User-Agent") || "").toLowerCase();
const cfRay = request.headers.get("cf-ray") || "unknown";
const botPatterns = [
"curl", "wget", "httpie", "python-requests", "scrapy", "postmanruntime",
"go-http-client", "java/", "libwww-perl", "okhttp", "python-urllib",
"apache-httpclient", "httpclient", "lwp::simple", "mechanize", "aiohttp",
"axios", "reqwest", "puppeteer", "headlesschrome", "phantomjs"
];
if (!userAgent.trim()) {
console.log(`[BLOCKED] Empty User-Agent - cf-ray: {cfRay}`);
return new Response("Forbidden: Empty User-Agent", { status: 403 });
}
if (botPatterns.some(bot => userAgent.includes(bot))) {
console.log(`[BLOCKED] Bot Detected:{userAgent} - cf-ray: {cfRay}`);
return new Response("Forbidden: Bot Detected", { status: 403 });
}
// **确定文件夹(PC / Mobile)**
let folder = "pc_img/"; // 默认 PC
if (pathname.endsWith("fallback_mobile.jpg")) {
folder = "mobile_img/"; // 强制移动端壁纸
} else if (pathname.endsWith("fallback_pc.jpg")) {
folder = "pc_img/"; // 强制 PC 端壁纸
} else {
// **`fallback.jpg` 需要根据 User-Agent 识别**
const isMobile = /iphone|ipod|android|blackberry|iemobile|opera mini/.test(userAgent);
folder = isMobile ? "mobile_img/" : "pc_img/";
}
console.log("Requested Path:", pathname);
console.log("Selected Folder:", folder);
// **初始化内存缓存(Map)**
const rateLimitCache = new Map(); // 用于缓存速率限制数据
// **速率限制(60 秒内最多 10 次请求)**
const clientIP = request.headers.get("CF-Connecting-IP") || "unknown";
const rateLimitKey = `rate-limit-{clientIP}`;
// 尝试从内存缓存获取速率限制数据
let currentCount = rateLimitCache.get(rateLimitKey);
if (!currentCount) {
// 如果缓存中没有,尝试从 KV 存储获取
try {
currentCount = await env.RATE_LIMIT_KV.get(rateLimitKey);
if (currentCount) {
currentCount = parseInt(currentCount);
} else {
currentCount = 0;
}
} catch (error) {
console.error("Error fetching rate limit from KV:", error);
// 如果 KV 读取失败,默认从 0 开始
currentCount = 0;
}
}
const newCount = currentCount + 1;
// 如果超出速率限制,返回 429 错误
if (newCount > 10) {
return new Response("Too Many Requests", { status: 429 });
}
// 更新内存缓存和 KV 存储
rateLimitCache.set(rateLimitKey, newCount);
try {
// 如果 KV 存储存在,则更新 KV 存储中的数据
await env.RATE_LIMIT_KV.put(rateLimitKey, newCount, { expirationTtl: 60 });
} catch (error) {
console.error("Error updating rate limit in KV:", error);
// 即使 KV 更新失败,也不影响请求继续处理
}
// **检查 Worker 使用情况**
const rateLimitUsageKey = "rate-limit-usage";
let rateLimitUsage = await env.RATE_LIMIT_KV.get(rateLimitUsageKey);
if (rateLimitUsage && rateLimitUsage > 90) {
console.log("Worker usage exceeds 90%, caching fallback.jpg at edge");
// **计算时间直到当天零点**
const now = new Date();
const midnight = new Date(now);
midnight.setHours(24, 0, 0, 0);
const ttl = (midnight.getTime() - now.getTime()) / 1000; // TTL until midnight in seconds
// **将 fallback.jpg 写入边缘缓存**
const fallbackImageUrl = "https://wallpaper.tangwudi.com/fallback.jpg";
const fallbackResponse = await fetch(fallbackImageUrl);
if (!fallbackResponse.ok) {
console.error("Failed to fetch fallback image for caching");
return new Response("Error caching fallback image", { status: 500 });
}
// 将 fallback.jpg 缓存到边缘,直到午夜
ctx.waitUntil(
caches.default.put(fallbackImageUrl, fallbackResponse.clone())
);
return fallbackResponse;
}
try {
// **获取 R2 存储桶中的文件列表**
const objects = await bucket.list({ prefix: folder });
const items = objects.objects;
if (items.length === 0) {
return new Response("No images found", { status: 404 });
}
// **随机选择一张图片**
const randomItem = items[Math.floor(Math.random() * items.length)];
if (!randomItem) {
return new Response("No valid images", { status: 404 });
}
const imageUrl = `https://wallpaper.tangwudi.com/${randomItem.key}`;
// **设置缓存策略**
const headers = new Headers();
headers.set("Cache-Control", "public, max-age=600"); // 浏览器缓存 10 分钟
headers.set("CDN-Cache-Control", "public, max-age=604800"); // Cloudflare CDN 缓存 7 天
headers.set("ETag", randomItem.etag);
// **302 重定向到图片 URL**
return Response.redirect(imageUrl, 302);
} catch (error) {
console.error("Error in Worker:", error);
return new Response("Internal Server Error", { status: 500 });
}
}
};
这段代码中,主要是新增了对worker免费额度的查询:如果超过90%,就会把fallback.jpg缓存到Cloudflare的CDN中(缓存时间由当前时间到凌晨0点的时间差来决定,因为0点会重置worker的免费额度,所以只需要坚持到那个时间点就够了~):
// **检查 Worker 使用情况**
const rateLimitUsageKey = "rate-limit-usage";
let rateLimitUsage = await env.RATE_LIMIT_KV.get(rateLimitUsageKey);
if (rateLimitUsage && rateLimitUsage > 90) {
console.log("Worker usage exceeds 90%, caching fallback.jpg at edge");
// **计算时间直到当天零点**
const now = new Date();
const midnight = new Date(now);
midnight.setHours(24, 0, 0, 0);
const ttl = (midnight.getTime() - now.getTime()) / 1000; // TTL until midnight in seconds
// **将 fallback.jpg 写入边缘缓存**
const fallbackImageUrl = "https://wallpaper.tangwudi.com/fallback.jpg";
const fallbackResponse = await fetch(fallbackImageUrl);
if (!fallbackResponse.ok) {
console.error("Failed to fetch fallback image for caching");
return new Response("Error caching fallback image", { status: 500 });
}
// 将 fallback.jpg 缓存到边缘,直到午夜
ctx.waitUntil(
caches.default.put(fallbackImageUrl, fallbackResponse.clone())
);
return fallbackResponse;
}
这样一来,由于在Cloudflare的流量序列中,缓存规则的优先级高于worker,所以后续对fallback.jpg的访问就不再能命中worker,而是命中了缓存规则,这样虽然暂时失去图片的随机效果,但是却保护了R2,而过了凌晨0点 worker额度重置以后又是一条好汉!
注1:目前还没机会测试这段代码到底可行不可行,能不能生效未知,且可能会造成KV每日免费额度的额外消耗 ,所以一般来说,大家使用v2.0版 代码就行了。
注2:在最新的v3.0版 代码中,正常使用还是只需要将背景图片指向https://wallpaper.tangwudi.com/fallback.jpg
,而如果要强制使用pc壁纸,只需要指向/fallback_pc.jpg
,而如果要强制只用移动端壁纸,就指向fallback_mobile.jpg
即可。
注3:如果不想worker的免费额度被轻易消耗完的话,建议设置好Cloudflare WAF的自定义规则来防盗链以及阻止对随机图片API的直接访问,从而实现只允许包含特定引用方的访问请求才能到达并被worker处理。
注4:这种优化还可以继续下去,比如速率限制再加上惩罚措施:60秒内超过10次访问拉黑多长时间;比如使用worker将图片转成webp/avif,并根据浏览器支持情况返回最佳格式;比如自定义图片规则(如早上返回阳光图片,晚上返回夜景);比如添加图片防盗链,返回默认占位图片等等。只不过,这些功能要么有现成的选项可以直接实现(Cloudflare的各种规则、wordpress的主题选项等),要么对一般的朋友来说其实并没有什么吸引力,所以实在没必要把worker代码整得太复杂,毕竟,复杂的功能实现是靠消耗worker免费请求额度来实现的,而免费请求数本来就是”正常时候用不完,不正常时候根本不够用”~,所以,文章中代码更新到v3.0版就足够了。
4 总结
自行搭建随机图片API具体到每个人的实际环境及喜好,具体的操作细节可能多多少少都会有一些改变,例如,如果觉得有些壁纸其实更适合白天模式,不适合深夜模式,不怕麻烦的朋友,还可以按照白天、晚上、pc、mobile进一步分为4个目录:/pc_img_day
、/pc_img_night
,/mobile_img_day
,mobile_img_night
,放入正确的图片,然后可以优化代码,根据具体的时间来判断是白天模式还是深夜模式,是pc还是mobile设备,进而返回正确的图片URL;再例如,为了每个月都用不同的图片,可以按照月份直接建立12个目录,每个目录里又分别建立pc和mobile目录等等。不管是本地存储方式还是云存储方式,不管是使用index.php还是cloudflare worker,都是这么个思路,需求确定之后,代码部分直接找AI就行,简单得很。
目前博客的背景图已经设置成基于worker的随机图片API了(v3.0版 代码),在Cloudflare上也能看到具体的worker数据:

另外,背景图片从固定地址换成随机图片API之后还有额外的发现,在几次测试后,PageSpeed Insights测试分数出现了有史以来最高分80分,真是意外之喜了:

考虑的很周到,学习了。我因为基本是自用(一些网页自用样式替换背景图),所以用的是微博图床储存图片,用CF woker实现随机图并反代修改Referer解决防盗链。也没做限流和图片缓存进CDN什么的。
其实限流、图片缓存CDN这些功能也不是必须的,在cloudflare上直接就可以实现。worker代码还是越简单其实越好,我后面那个v3.0版本的worker代码上线后都收到了kv使用超过50%的邮件通知了~~。
公开API还是考虑各种限制为好,之前我的图床(后端对接的是OneDrive)没做好速率限制,因为API速度率反复超限,被微软直接把整个组织扬了,损失了4位数的子账户
公共的那肯定了,我说的仅仅是自用的情况,如果要放开,那考虑的就不一样了。