用 Cloudflare Worker + KV 做博客点赞统计
静态博客没有后端也能有全局点赞数:一个 Worker、一个 KV 命名空间,加上「只追加」的计数设计,几十行代码跑通。
静态站点最尴尬的地方,是「没有后端」。评论可以交给 Giscus,浏览量可以交给 Web Analytics,但点赞这种需要写数据的小功能,往往就卡住了。
这篇记录我用 Cloudflare Worker + KV 把它做完的完整思路。
为什么不直接读写一个计数器
最直觉的做法是:读 KV 里的 count,加一,再写回去。
const n = Number(await LIKES.get('count:' + path)) || 0;
await LIKES.put('count:' + path, String(n + 1));
问题在于 KV 是最终一致的:写完之后在别的边缘节点不一定立刻可见。两个人同时点赞,很可能都读到 n = 5,都写回 6,一次点赞就这么丢了。
改成「只追加」
既然覆盖写会丢,那就不要覆盖。每次点赞写一个独立的 key:
like:<path>:<timestamp>:<random>
计数时按前缀数 key 的个数:
async function countLikes(path) {
const prefix = 'like:' + path + ':';
let total = 0, cursor;
do {
const page = await LIKES.list({ prefix, cursor });
total += (page.keys || []).length;
cursor = page.list_complete ? undefined : page.cursor;
} while (cursor);
return total;
}
因为 key 名里带了时间戳和随机数,两次点赞永远不会写到同一个 key 上,并发再高也不会互相覆盖。代价是 key 数量会线性增长,但对个人博客这个量级完全够用。
Worker 的两个接口
addEventListener('fetch', (event) => {
event.respondWith(handle(event.request));
});
async function handle(req) {
const url = new URL(req.url);
// 允许的站点
const cors = {
'Access-Control-Allow-Origin': 'https://my-blog-jingpanlong.pages.dev',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (req.method === 'OPTIONS') return new Response(null, { headers: cors });
if (req.method === 'GET') {
const path = url.searchParams.get('path') || '/';
return json({ count: await countLikes(path) }, cors);
}
if (req.method === 'POST') {
const path = await req.text();
const key = `like:${path}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
await LIKES.put(key, '1');
return json({ count: await countLikes(path) }, cors);
}
}
LIKES 是绑定到 Worker 上的 KV 命名空间,在 wrangler.toml 里声明即可。
前端只做三件事
- 进页面
GET一次,把总数显示出来; - 点过赞就把路径存进
localStorage,按钮置为已赞,不重复提交; - 点的时候
POST一次,用返回的count覆盖显示。
后端不关心「谁」点的,也就没有用户系统和隐私负担。对个人博客来说,这正是想要的效果。
踩到的两个坑
workers.dev子域没开:Worker 部署成功但访问报error code: 1042,需要在控制台把workers.dev路由打开。curl测试计数一直是 0:不是 Worker 的问题,是 Git Bash 把/posts/xxx/当成路径改写成了 Windows 本地路径,导致 key 前缀对不上。测试时加上MSYS2_ARG_CONV_EXCL='*'就好了。
整套东西没有服务器、没有数据库,免费额度绰绰有余。静态博客想要一点「动态」,这大概是最省心的做法。
评论区
使用 GitHub 账号登录即可留言。若评论未显示,请先在仓库开启 Discussions 并安装 Giscus App。