代码与工具 5 分钟阅读

给静态站加一层 API

静态站有个绕不开的尴尬:想加个动态功能,就得动服务器。我最近想给站内加个「随机文章」接口,顺便统计一下点击,但不想为了这点事去维 […]

给静态站加一层 API

静态站有个绕不开的尴尬:想加个动态功能,就得动服务器。我最近想给站内加个「随机文章」接口,顺便统计一下点击,但不想为了这点事去维护一台 VPS。Cloudflare Workers 正好补上这个缺口——它跑在边缘,免费额度够个人站用,而且能直接和静态站绑在同一个域名下。

先想清楚要什么

我的需求很小:一个 /api/random 返回随机文章链接,一个 /api/hit 记录某篇文章被点开过。数据量不大,用 Workers KV 存就够了。KV 是键值存储,读多写少,适合这种场景。先别急着写代码,把路由和数据结构定下来:KV 里存 hits:<slug> 作为键,值是次数;随机文章列表直接硬编码在 Worker 里,反正文章不多,每次部署时更新一下就行。

把 Worker 挂到域名下

在 Cloudflare 后台新建一个 Worker,然后去 DNS 里加一条路由。我的站是 quq.pub,静态资源在 Pages 上,Worker 路由设成 quq.pub/api/*,这样只有 /api 开头的请求会进 Worker,其他照旧走 Pages。路由规则写 quq.pub/api/* 就行,注意别写成 quq.pub/*,那样会把所有请求都劫持过来,静态资源全挂。

Worker 代码用 ES Module 格式,入口导出一个 fetch 函数。先写个最简单的:

export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/api/random') {
const posts = ['/post/a', '/post/b', '/post/c'];
const pick = posts[Math.floor(Math.random() * posts.length)];
return new Response(JSON.stringify({ url: pick }), {
headers: { 'content-type': 'application/json' }
});
}
return new Response('Not found', { status: 404 });
}
}

部署后访问 quq.pub/api/random 就能看到 JSON 了。这一步跑通,后面就是加功能。

用 KV 记点击

在 Worker 的设置里绑定一个 KV 命名空间,变量名比如 HITS。然后加一个 /api/hit 路由,接收 POST,body 里带 slug。逻辑很简单:读当前值,加一,写回去。KV 的写有延迟,但个人站不在乎这点实时性。

if (url.pathname === '/api/hit' && request.method === 'POST') {
const { slug } = await request.json();
const key = hits:${slug};
const current = parseInt(await env.HITS.get(key) || '0', 10);
await env.HITS.put(key, String(current + 1));
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' }
});
}

前端在文章页里加一小段脚本,页面加载时 fetch('/api/hit', { method: 'POST', body: JSON.stringify({ slug: '...' }) })。注意别在每次滚动或交互时都发,只发一次就行。

几个容易踩的坑

第一个是 CORS。如果前端和 API 同域,其实不需要额外头;但如果本地开发时前端跑在 localhost:4321,请求打到线上 Worker,就会跨域。我图省事,在 Worker 里统一加了 access-control-allow-origin: *,个人站不涉及敏感数据,够用。

第二个是缓存。Cloudflare 默认会缓存 GET 请求,/api/random 如果被缓存了,每次返回都一样。在响应头里加 cache-control: no-store 就能绕开。

第三个是 KV 的最终一致性。刚写入的值,立刻读可能还是旧的。所以别在同一个请求里写完马上读来做判断,分开两次请求就没事。

还能往哪走

这套骨架搭好之后,加功能就是填路由的事。我后面打算加一个 /api/search,用 Workers 的 HTMLRewriter 在边缘把静态页的标题和摘要抽出来,拼成搜索索引返回。再远一点,可以接上 D1 存文章元数据,做分页列表。但那是下一步的事了,现在这个 /api/random 已经能让首页的「随便看看」按钮跑起来,我觉得挺值。

静态站不必是死的。加一层薄薄的 API,它就能有呼吸。

留下你的回声