用 Cloudflare 免费方案建立自己的图库,真的 $0 起步
你有没有想过,把自己常用的免费商用图片集中管理,建一个自己的图库入口,搜索一下就能找到——而且完全不花钱?
我们做了,这篇文章分享整个架构怎么搭。
先说为什么要自己建
做设计、做网站、做营销,免不了找图。Pixabay、Pexels、Unsplash 都很好用,但每次都要各自打开、各自搜索,时间碎片化。
更烦的是:你用过一张图,下次根本不记得在哪找到的。
如果能有一个自己的图库入口,把常用来源全部整合进去,搜索一次横扫所有来源,那才是省时间的方式。
这就是我们做 FreePicHub 自由图汇 的出发点。
Cloudflare 免费额度有多够用
很多人不知道 Cloudflare 有多慷慨。以下是这个项目实际用到的服务,全部在免费方案内:
| 服务 | 用途 | 免费额度 |
|---|---|---|
| R2 对象存储 | 存图片文件 | 10 GB 存储 / 每月 1,000 万次 GET |
| D1 数据库 | 存图片 metadata | 5 GB 存储 / 每日 500 万次读取 |
| Workers | API 后端 | 每日 10 万次请求 |
| Pages | 前端网站 | 无限静态部署 |
一个小型到中型的个人图库,这个额度完全够用,而且不需要信用卡就能开始。
整体架构
[Python 自动化导入脚本]
↓
图片处理 (WebP 压缩)
↓
┌──────────────────┐
│ Cloudflare │
│ │
│ R2 (图片文件) │
│ D1 (数据库) │
│ Workers (API) │
│ Pages (前端) │
└──────────────────┘
↓
浏览器用户四个 Cloudflare 服务各司其职:
- R2:放压缩过的图片(缩略图 / 预览图 / 原图)
- D1:SQLite 数据库,存标题、尺寸、标签、图片的 R2 路径
- Workers:写两支 API —
/api/search和/api/featured - Pages:静态前端,纯 HTML + CSS + JS,无框架依赖
Step 1:建立 R2 Bucket
进 Cloudflare Dashboard → R2 → 创建 Bucket,名称自定(例如 my-image-library)。
接着到 Manage R2 API Tokens,创建一个 User API Token:
- 权限选「Object Read & Write」
- 限定套用到你的 Bucket
记下 Access Key ID、Secret Access Key、账号 ID,之后 Python 脚本会用到。
R2 是 S3 兼容 API,所以 Python 端用 boto3 就能直接操作,不需要学新的 SDK。
Step 2:建立 D1 数据库
在 Wrangler CLI 执行:
npx wrangler d1 create my-image-db记下返回的 database_id。然后建立 Schema:
CREATE TABLE IF NOT EXISTS images (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
source_id TEXT,
title TEXT,
tags TEXT DEFAULT '[]',
width INTEGER,
height INTEGER,
aspect_ratio TEXT,
license_type TEXT NOT NULL,
thumb_key TEXT,
preview_key TEXT,
original_key TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_source ON images(source);
CREATE INDEX IF NOT EXISTS idx_created ON images(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_source_id ON images(source, source_id);npx wrangler d1 execute my-image-db --file=schema.sqlStep 3:Workers API(用 Pages Functions)
最方便的做法是用 Cloudflare Pages Functions,直接把 JS 放在 /functions 目录下,Cloudflare 会自动部署成 Worker。
搜索 API (functions/api/search.js):
export async function onRequestGet({ request, env }) {
const url = new URL(request.url)
const q = url.searchParams.get('q') || ''
const source = url.searchParams.get('source') || ''
const page = parseInt(url.searchParams.get('page') || '1')
const limit = 24
const offset = (page - 1) * limit
let sql = 'SELECT * FROM images WHERE 1=1'
let params = []
if (q) {
sql += ' AND (title LIKE ? OR tags LIKE ?)'
params.push(`%${q}%`, `%${q}%`)
}
if (source) {
sql += ' AND source = ?'
params.push(source)
}
sql += ` ORDER BY created_at DESC LIMIT ${limit + 1} OFFSET ${offset}`
const { results } = await env.MY_DB.prepare(sql).bind(...params).all()
const hasMore = results.length > limit
return Response.json({
results: results.slice(0, limit).map(r => ({
...r,
tags: JSON.parse(r.tags || '[]')
})),
has_more: hasMore
}, { headers: { 'Access-Control-Allow-Origin': '*' } })
}Step 4:图片处理与自动化导入
这是整个系统最有趣的部分。写一支 Python 脚本,把图片处理后上传到 R2,metadata 写入 D1。
图片处理:每张图产出三个版本,全部存成 WebP
from PIL import Image
def process_image(source_path, output_dir):
img = Image.open(source_path).convert("RGB")
w, h = img.size
# 缩略图 512px(前端列表用)
thumb = img.copy()
thumb.thumbnail((512, 512), Image.LANCZOS)
thumb.save(output_dir / "thumb.webp", "WEBP", quality=82)
# 预览图 1024px(点开 modal 用)
preview = img.copy()
preview.thumbnail((1024, 1024), Image.LANCZOS)
preview.save(output_dir / "preview.webp", "WEBP", quality=85)
# 原图转 WebP 压缩后保存
img.save(output_dir / "original.webp", "WEBP", quality=90)
return {"width": w, "height": h}WebP 格式比 JPEG 通常小 25–35%,长期下来 R2 存储成本差很多。
上传到 R2(boto3 S3 兼容接口):
import boto3
s3 = boto3.client(
"s3",
endpoint_url=f"https://{ACCOUNT_ID}.r2.cloudflarestorage.com",
aws_access_key_id=ACCESS_KEY,
aws_secret_access_key=SECRET_KEY,
region_name="auto",
)
s3.put_object(Bucket="my-image-library", Key="source/uuid/thumb.webp", Body=file_bytes)写入 D1(防止重复写入的关键):
import requests, uuid, json
def write_to_d1(record):
resp = requests.post(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/d1/database/{DB_ID}/query",
headers={"Authorization": f"Bearer {D1_TOKEN}"},
json={
"sql": """
INSERT INTO images (id, source, source_id, title, tags,
width, height, aspect_ratio, license_type,
thumb_key, preview_key, original_key)
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM images WHERE source=? AND source_id=?
)
""",
"params": [str(uuid.uuid4()), record["source"], record["source_id"],
record["title"], json.dumps(record["tags"]),
record["width"], record["height"], record["aspect_ratio"],
record["license_type"], record["thumb_key"],
record["preview_key"], record["original_key"],
record["source"], record["source_id"]]
}
)
return resp.json().get("success", False)WHERE NOT EXISTS 让你重复执行脚本也不会写入重复数据——这个设计让整个系统可以安心地每天定时跑。
Step 5:前端部署
前端三个静态文件:index.html、style.css、app.js,推上 Cloudflare Pages:
npx wrangler pages deploy ./frontend --project-name my-image-libraryCSS masonry 瀑布流排版,完全不需要 JS 套件:
.image-grid {
columns: 4 220px;
gap: 14px;
}
.image-card {
break-inside: avoid;
margin-bottom: 14px;
}成本试算
以每天导入 500 张、图库规模 10,000 张计算(缩略图约 50KB、预览图约 200KB):
| 项目 | 用量 | 费用 |
|---|---|---|
| R2 存储 | ≈ 2.5 GB | $0 |
| D1 读取(日访客 1,000 人) | 5,000 次/日 | $0 |
| Workers 请求 | 5,000 次/日 | $0 |
| Pages 部署 | 无限次 | $0 |
| 总计 | $0 / 月 |
Cloudflare 的免费额度对个人或小型商业图库来说几乎用不完。等规模到几十万张、日访客破万,再评估要不要升级方案。
小结
整个系统的核心思想很简单:
- R2 负责存文件,便宜又快
- D1 负责存索引,搜索靠它
- Workers 负责查询逻辑
- Pages 负责呈现给用户
- Python 脚本 负责定期喂数据进去
全部跑在 Cloudflare edge network 上,全球加速,零维护成本。
如果你也想建一个自己的图库,技术门槛其实没有想象中高——只要会基本的 Python 和 JavaScript,这套架构完全可以在一个周末内跑起来。
实际成品可以参考:FreePicHub 自由图汇
有技术规格或架构问题想咨询?
欢迎来 ascentek.info 联系我们,无论是 Cloudflare 架构设计、Python 自动化流程,或是图库建置规划,我们很乐意讨论。
延伸阅读