2046 字
10 分钟
也许是最适合个人博客的图床方案

前言#

今天来聊聊一个老生常谈的话题,博客图床。动态博客框架一般都具备图片管理功能,而静态博客一般会选择把图片放在资源文件夹,跟博客一起分发,反正都是静态资源。那为什么还需要专门的图床呢?专门的图床能更好的集中管理图片,可以分发到不同的平台,可以不受博客框架约束,可以避免博客仓库体积过大,可以单独加速图片的分发。

本文推荐的是我认为最适合个人博客的图床方案:Cloudflare R2 + Worker + Images,完全免费,操作简单,也不用担心被刷欠费。方案是在前人基础上改进的,现在互联网上的很多方案都是 Cloudflare R2 + Webp Cloud,但是实际上 Cloudflare 本身就有类似 Webp Cloud 提供的功能,即 Cloudflare Images。我推荐的方案解决了 R2 被刷的风险,而且相比基础方案部署更方便,只需要一个 Cloudflare 账户和一个域名即可,还省去了配置 WAF 的麻烦。

CF Images v.s. Webp Cloud#

不难看出,相比基础方案,最大的改动就是使用 Cloudflare Images 代替了 Webp Cloud,那么 Cloudflare Images 对比 Webp Cloud 有什么好处呢?建立在都是用 R2 存储的基础上,我们来对比一下二者的免费计划。

接入方式

都使用 R2 免费存储,免费配额是 10 GB 存储、100 万次 Class A、1000 万次 Class B 操作每月,传输流量免费。Webp 接入 R2 需要将存储桶设置为公开访问,并且需要为 R2 单独绑定域名,为了防止被刷还需要设置 WAF 规则。Images 可以接入 R2 私有存储桶,完全不用担心 R2 被刷,不需要额外绑定域名,更不需要设置 WAF 规则,此为 Images 一胜。

免费额度

WebP 每天 3000 次成功图片请求,不管缓存是否命中都会计入请求次数(缓存命中算 0.5 次请求)。如果你的博客图片较多,很多人访问,那么日 3000 次额度很容易用完。更麻烦的是,如果你的网站被无聊的人盯上,3000 次请求更是很容易被刷完。当你额度用完时,你要么选择 302 重定向,要么选择不展示图片。如果选择 302 重定向,就会暴露源站地址;如果源站有 WAF 规则,那么 302 重定向也没有意义。

Webp Cloud 超额时的行为设定

而 Images 的计费方式不同,Images 提供每月 5000 次的图片变换,并且是按原图 + 参数组合的唯一变换来算,不是按请求次数算。也就是说,对于同一张图片的相同参数请求,即使你一个月请求个几万次,都只计入一次图片变换。更重要的是,变换后的图片还能利用 CF 的全球(环大陆)缓存功能。此为 Images 一胜。

缓存与 SLA

Images 的变换结果会缓存到 Cloudflare 网络中,你可以自己设置缓存 TTL(最短 1 小时),至于配额虽然 CF 没有明确说明,但本质上还是 CDN 缓存。即使你的缓存因为某种原因被边缘节点删除了,无非就是图片响应速度会变慢点,完全不影响你的其他额度。而 Webp 的免费缓存额度是 200MB,虽然对于个人博客来说完全够用,但没有找到 TTL 的设置。

至于 SLA,其实二者都是建立在 Cloudflare 基础设施上的。是的,Webp 也是 Cloudflare 的客户,但是由于多了 Webp 这么一个中间层,相当于多个一个潜在的故障风险。

总结

要我说啊,Cloudflare 简直就是大善人,赛博活菩萨啊。说了这么多,看起来似乎是 Cloudflare Images 赢麻了,那么 Webp Cloud 就一点优点都没有吗?我看未必,Webp 可以更方便的接入非 R2 存储的源站。另外,Webp 所使用的节点似乎是 CF 商业订阅的节点,大陆地区的可访问性可能相对较好。如果你希望提升大陆地区的访问体验,可以在 Images 前面再套一层 Webp。(bushi

部署#

创建 R2 存储桶#

登录 Cloudflare Dashboard,左侧导航栏点击 Storage & databases -> R2 Object Storage -> Overview,然后点击页面的 Create bucket 按钮。随便填一个名称都行,然后创建存储桶即可,之后不需要设置什么东西,下一步部署 worker 即可。

创建 R2 存储桶

部署 Worker#

登录 Cloudflare Dashboard,左侧导航栏点击 Compute -> Workers & Pages,然后点击页面的 Create application,选择 Start with Hello World! ,然后名称随便填一个就行。

Start with Hello World!

接下来是最关键的部分,需要添加绑定存储桶和 Images。点击你刚才创建的 Worker,进入详情页面。然后点击 Tab 栏的 Bindings -> Add binding,选择 R2 bucketVariable name 严格填入 BUCKET,下方选择你的存储桶即可。

image

类似的,添加 Images 绑定,Variable name 严格填入 IMAGES

image

在完成之后,你的 Bindings 页面看起来应该类似这样。

绑定存储桶和Images

添加绑定之后,点击 Edit code,将 worker 的模板代码全部删掉,替换为我提供的代码(见附录) ,你可以自行修改一些配置,如压缩质量、缓存 TTL 等。最重要的是别忘记修改 ALLOWED_HOSTNAME_REGEX ​为你的网站,如果不知道怎么改,去问 AI 吧。最后你可以为 worker 添加自定义域名,例如 img.yourblog.com。当然,这是可选的,你也可以直接使用 CF 提供的免费子域名,但这个域名在大陆地区似乎被阻断了。如果你想进一步提升大陆的访问体验,可以进行优选或再添加一个 Webp Cloud 前置。

Deploy Worker

配置 Piclist#

这一步是可选的,而且网上已经有很多教程了,所以只简单说一下。下载 PicList,配置 AWS S3 图床。

配置名:随便填,比如 R2
AccessKeyID:你的 R2 Access Key ID
SecretAccessKey:你的 R2 Secret Access Key
Bucket:你的R2桶名
上传路径:默认即可
Region:留空即可
自定义节点:https://<ACCOUNT_ID>.r2.cloudflarestorage.com
自定义域名:https://你的图片访问域名

附录#

Worker 代码#

// 配置压缩参数和缓存TTL
const CONFIG_QUALITY = 80;
const CONFIG_TTL = 604800;
// 匹配: kasuha.com, localhost 等
const ALLOWED_HOSTNAME_REGEX = /^([a-zA-Z0-9-]+\.)*(kasuha\.com|localhost)$/i;
// 非法路径正则:防止目录穿越及控制字符
const INVALID_PATH_REGEX = /(?:\.\.|[\x00-\x1F\x7F])/;
export default {
async fetch(request, env, ctx) {
if (request.method !== 'GET') {
return new Response('Method Not Allowed', { status: 405 });
}
const url = new URL(request.url);
const rawPath = url.pathname.slice(1);
const key = decodeURIComponent(rawPath);
if (!key || INVALID_PATH_REGEX.test(key)) {
return new Response('Bad Request', { status: 400 });
}
// 极速防盗链与 CORS 验证
const refererStr = request.headers.get('Referer') || request.headers.get('Origin');
let isAllowed = false;
let requestOrigin = '';
if (refererStr) {
try {
// 解析 Referer 为 URL 对象提取 hostname,避免正则处理复杂的完整 URL
const refUrl = new URL(refererStr);
requestOrigin = refUrl.origin;
if (ALLOWED_HOSTNAME_REGEX.test(refUrl.hostname)) {
isAllowed = true;
}
} catch (e) {
// 如果 Referer 连合法的 URL 都不是,直接判定为非法
isAllowed = false;
}
if (!isAllowed) {
return new Response('Forbidden', { status: 403 });
}
} else {
// 允许无 Referer 的直接访问(例如浏览器地址栏直接敲回车)
// 如果你想禁止空 Referer,把这里改为 false 并 return 403
isAllowed = true;
}
// 格式判断与规范的 Cache Key
let targetFormat = 'image/webp';
const cacheUrl = new URL(url.origin + url.pathname);
cacheUrl.searchParams.set('f', targetFormat);
const cacheRequest = new Request(cacheUrl.toString(), request);
const cache = caches.default;
let cachedResponse = await cache.match(cacheRequest);
if (cachedResponse) {
const response = new Response(cachedResponse.body, cachedResponse);
response.headers.set('X-Cache-Status', 'HIT');
return response;
}
// 核心业务:R2读取与图片压缩
try {
const sourceObject = await env.BUCKET.get(key);
if (!sourceObject) {
return new Response('Not Found', { status: 404 });
}
const imageResult = await env.IMAGES.input(sourceObject.body)
.transform({ quality: CONFIG_QUALITY })
.output({ format: targetFormat });
const response = imageResult.response();
const headers = new Headers(response.headers);
// 安全响应头与精确 CORS
if (isAllowed && requestOrigin) {
headers.set('Access-Control-Allow-Origin', requestOrigin);
headers.set('Vary', 'Origin');
}
headers.set('Cache-Control', `public, max-age=${CONFIG_TTL}`);
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('X-Cache-Status', 'MISS');
const finalResponse = new Response(response.body, {
status: response.status,
headers
});
if (CONFIG_TTL > 0) {
ctx.waitUntil(cache.put(cacheRequest, finalResponse.clone()));
}
return finalResponse;
} catch (e) {
return new Response('Internal Server Error', { status: 500 });
}
},
};

简单测试#

压力测试

Webp Cloud 测速

Worker 无优选测速

参考资料#

也许是最适合个人博客的图床方案
https://kasuha.com/posts/optimal-image-host-for-blog/
作者
霞葉
发布于
2026-03-22
许可协议
CC BY-NC-SA 4.0
评论将在滚动到此处后加载...