家庭数据中心系列 构建高效且安全的随机图片API:Cloudflare Worker + R2 + KV 实战指南

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 和移动端)

实现步骤

  1. 在云存储(Cloudflare R2 / S3 /Github)中存放图片,并确保它们可以通过 HTTP 访问。
  2. 在 VPS 上创建 pc_img.txt 文件,存储图片的链接,每行一张图片的 URL。
  3. VPS 端 PHP 读取 pc_img.txt 并随机返回一张图片的 URL
  4. (可选)使用脚本定期更新 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”的功能就很合理了:

image.png

image.png


文章前面提到基于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的存储桶。


按照如下图片教程操作:

image.png

image.png

image.png

选择”设置”选项卡:
image.png

配置自定义域名:
image.png

image.png

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

3.2.2.2 创建KV

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

image.png

image.png

完成:
image.png

之所以选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

image.png

image.png

image.png

3.2.3.2 编辑代码

image.png

将以下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
    }
  }
};

如下图:

image.png


这段代码的主要功能是”根据访问设备的类型(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存储桶

image.png

image.png

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

WALLPAPER_BUCKET

如下图:

image.png

3.2.3.4 添加入口路由

image.png

image.png

image.png

最终完成后:

image.png

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的访问次数的统计内容):

image.png

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

RATE_LIMIT_KV

如下图:

image.png

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

这段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的缓存规则(或者页面规则,但是页面规则来实现缓存有点浪费,没啥必要)来实现,简单明了,如下图:

image.png


如果同时开启了Cloudflare的图像优化功能,比如Polish、Mirage,则可能导致本地浏览器缓存10分钟的策略失效:

PolishMirage 都有可能改变图片的内容或格式,这可能导致浏览器和 CDN(Cloudflare)缓存不命中,进而导致每次加载新图片。

Polish 可能导致不同格式的图片(如 WebP 和原始格式),并且每次请求时可能不同,这会导致缓存未命中。

Mirage 则可能因为设备和网络条件的不同,返回不同版本的图片,导致缓存不一致。


4、去掉代码基于查询参数”?type=”的判断,改为静态路径的判断

从安全角度考虑,使用静态路径(比如/fallback_pc.jpg/fallback_mobile.jpg)替代查询参数,可以使 Worker 路由更加简洁(wallback.tangwudi.com/*太过于粗旷了):只需要这三条具体的路由即可明确区分设备类型,避免了对查询参数的依赖:

image.png

同时,对随机图片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(对应前面的访问),如下图:

image.png

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

image.png

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

image.png

那么对背景图片的访问就从之前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_daymobile_img_night,放入正确的图片,然后可以优化代码,根据具体的时间来判断是白天模式还是深夜模式,是pc还是mobile设备,进而返回正确的图片URL;再例如,为了每个月都用不同的图片,可以按照月份直接建立12个目录,每个目录里又分别建立pc和mobile目录等等。不管是本地存储方式还是云存储方式,不管是使用index.php还是cloudflare worker,都是这么个思路,需求确定之后,代码部分直接找AI就行,简单得很。

目前博客的背景图已经设置成基于worker的随机图片API了(v3.0版 代码),在Cloudflare上也能看到具体的worker数据:

image.png

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

image.png

博客内容均系原创,转载请注明出处!更多博客文章,可以移步至网站地图了解。博客的RSS地址为:https://blog.tangwudi.com/feed,欢迎订阅;如有需要,可以加入Telegram群一起讨论问题。

评论

  1. Windows Chrome 133.0.0.0
    3 周前
    2025-2-17 12:05:26

    考虑的很周到,学习了。我因为基本是自用(一些网页自用样式替换背景图),所以用的是微博图床储存图片,用CF woker实现随机图并反代修改Referer解决防盗链。也没做限流和图片缓存进CDN什么的。

    • 博主
      秋风于渭水
      Macintosh Chrome 132.0.0.0
      3 周前
      2025-2-17 13:42:24

      其实限流、图片缓存CDN这些功能也不是必须的,在cloudflare上直接就可以实现。worker代码还是越简单其实越好,我后面那个v3.0版本的worker代码上线后都收到了kv使用超过50%的邮件通知了~~。

      • tangwudi
        Windows Chrome 133.0.0.0
        3 周前
        2025-2-18 14:46:34

        公开API还是考虑各种限制为好,之前我的图床(后端对接的是OneDrive)没做好速率限制,因为API速度率反复超限,被微软直接把整个组织扬了,损失了4位数的子账户

        • 博主
          秋风于渭水
          Macintosh Chrome 132.0.0.0
          3 周前
          2025-2-18 14:50:41

          公共的那肯定了,我说的仅仅是自用的情况,如果要放开,那考虑的就不一样了。

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
       
error:
zh_CN