用 Cloudflare Worker 构建博客流量分析 API——从入门到生产级脚本
前言
在上一篇给我的博客添加监控板块:使用 Cloudflare API 获取网站流量信息中,我们介绍了 Cloudflare Analytics 的基本概念、GraphQL API 的调用方式,以及如何创建一个 Hello World 级别的 Worker。
但生产环境和 Hello World 之间,隔着不少距离:
- 如何一次查询多个维度的数据(国家、设备、浏览器、OS、缓存状态、HTTP 协议)?
- 如何拉取 30 天的细分数据而不触发 API 限频?
- 如何让 POST 请求也能被 Cloudflare CDN 缓存?
- 返回的数据如何直接被前端图表库消费?
本篇将基于我博客实际在用的 cf-analytics-worker.js 脚本,完整拆解一个生产级流量分析 Worker 的设计与实现。
本 Worker 服务于 lxpavilion.top/analytics,每日处理博客流量数据汇总与展示。
一、项目结构速览
项目只有两个核心文件:
workers/
├── cf-analytics-worker.js # Worker 入口脚本(约 270 行)
└── wrangler.json # 部署配置
wrangler.json
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cf-analytics",
"main": "cf-analytics-worker.js",
"compatibility_date": "2026-06-10",
"placement": { "mode": "smart" },
"vars": {
"CF_API_TOKEN": "cfut_YOUR_TOKEN_HERE",
"CF_ZONE_ID": "your_zone_id_here"
}
}
两个关键环境变量:
CF_ZONE_ID:你的域名 Zone IDCF_API_TOKEN:具有 Analytics Read 权限的 API Token
⚠️ 安全提醒:上面写
vars只是为了方便演示。生产环境请务必使用wrangler secret put CF_API_TOKEN单独加密存储,不要硬编码在配置文件中。
二、核心查询设计:三条 GraphQL 查询撑起所有数据
整个 Worker 的核心是三条 GraphQL 查询语句,分别负责不同时间粒度和维度的数据。
2.1 日级 + 小时级数据(一次请求)
函数 buildDailyHourlyQuery 通过 GraphQL 的别名(alias)机制,在一次请求中同时获取日级和小时级数据:
function buildDailyHourlyQuery(zoneId, days) {
const endDate = new Date();
const startDate = new Date(Date.now() - days * 86400000);
const startStr = startDate.toISOString().slice(0, 10);
const endStr = endDate.toISOString().slice(0, 10);
// 小时数据只能查最近 3 天
const hStart = new Date(Math.max(startDate.getTime(), Date.now() - 3 * 86400000));
const hStartDt = hStart.toISOString().replace(/\.\d{3}Z$/, "Z");
const endDt = endDate.toISOString().replace(/\.\d{3}Z$/, "Z");
return `{
viewer {
zones(filter: {zoneTag: "${zoneId}"}) {
daily: httpRequests1dGroups(
limit: ${Math.min(days + 1, 365)},
filter: {date_geq: "${startStr}", date_leq: "${endStr}"},
orderBy: [date_ASC]
) {
dimensions { date }
sum { requests cachedRequests cachedBytes bytes pageViews }
uniq { uniques }
}
hourly: httpRequests1hGroups(
limit: 120,
filter: {datetime_gt: "${hStartDt}", datetime_lt: "${endDt}"},
orderBy: [datetime_ASC]
) {
dimensions { datetime }
sum { requests cachedRequests bytes pageViews }
uniq { uniques }
}
}
}
}`;
}
要点:
| 字段 | 含义 | 时间范围 |
|---|---|---|
daily |
每日汇总(请求数、缓存、带宽、浏览量、独立访客) | 最长 364 天 |
hourly |
每小时粒度数据 | 最长 3 天(Cloudflare API 限制) |
orderBy: [date_ASC] |
按日期升序排列,前端直接渲染折线图无需再排序 |
2.2 六维度细分数据(按天粒度)
函数 buildBreakdownQuery 针对某一天的请求,请求 6 个维度的分布情况:
function buildBreakdownQuery(zoneId, dateStr) {
const startMs = new Date(dateStr).getTime();
const endMs = startMs + 86400000;
return `{
viewer {
zones(filter: {zoneTag: "${zoneId}"}) {
byCountry: httpRequestsAdaptiveGroups(
limit: 100,
filter: {datetime_geq: "${new Date(startMs).toISOString()}",
datetime_lt: "${new Date(endMs).toISOString()}"},
orderBy: [count_DESC]
) {
count
dimensions { clientCountryName }
}
byDevice: httpRequestsAdaptiveGroups(limit: 10, ...) {
count dimensions { clientDeviceType }
}
byBrowser: httpRequestsAdaptiveGroups(limit: 10, ...) {
count dimensions { userAgentBrowser }
}
byOS: httpRequestsAdaptiveGroups(limit: 10, ...) {
count dimensions { userAgentOS }
}
byCache: httpRequestsAdaptiveGroups(limit: 10, ...) {
count dimensions { cacheStatus }
}
byHTTP: httpRequestsAdaptiveGroups(limit: 10, ...) {
count dimensions { clientRequestHTTPProtocol }
}
}
}
}`;
}
六个维度一览:
| GraphQL 别名 | 维度 | 字段 | 说明 |
|---|---|---|---|
byCountry |
国家分布 | clientCountryName |
访客来自哪些国家 |
byDevice |
设备类型 | clientDeviceType |
desktop / mobile / tablet |
byBrowser |
浏览器 | userAgentBrowser |
Chrome / Firefox / Safari 等 |
byOS |
操作系统 | userAgentOS |
Windows / macOS / iOS / Android |
byCache |
缓存状态 | cacheStatus |
hit / miss / dynamic |
byHTTP |
HTTP 协议 | clientRequestHTTPProtocol |
HTTP/1.1 / HTTP/2 / HTTP/3 |
注意:
httpRequestsAdaptiveGroups的时间范围上限是 24 小时,无法一次性查询过去一周的按国家分布。这意味着如果要展示"近 7 天访客国家分布",需要逐天请求再自行汇总。
三、并发控制:pMapSerial 的原理与实现
逐天请求带来一个问题:如果需要拉取 30 天的 6 维度数据,就要发送 30 次 GraphQL 请求。全部串行太慢,全部并发又可能触发 Cloudflare API 速率限制。
解决方案是 pMapSerial——按并发度分片执行:
async function pMapSerial(items, concurrency, fn) {
const results = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency).map((item, idx) =>
fn(item, i + idx).catch(() => null) // 单条失败不阻塞整体
);
const batchResults = await Promise.all(batch);
results.push(...batchResults);
}
return results;
}
为什么不用 Promise.all 全量并发?
因为 Cloudflare GraphQL API 有速率限制,一次性发出 30 个并行请求很可能被限频(429 Too Many Requests)。pMapSerial 以 5 路并发逐批执行,既提升了速度,又留有余量。
实际调用代码:
const zones = await pMapSerial(dayDates, 5, async (dateStr) => {
const bJson = await callCF(buildBreakdownQuery(env.CF_ZONE_ID, dateStr), env);
return bJson?.data?.viewer?.zones?.[0] || null;
});
每条失败时返回 null 而不是抛异常——这种容错设计确保某一天的数据拉取失败不会让整个请求崩溃。
四、缓存策略:用 SHA-256 让 POST 请求也能被缓存
挑战
Worker 的接口是 POST 方法(通过 body 传 { days: 7 }),而 Cloudflare CDN 默认不会缓存 POST 请求。如果不用缓存,每次前端刷新页面都会触发一次完整的 API 调用链(最多 30 次 GraphQL 请求),既不经济也不快。
解决方案
利用 Workers 的 Cache API,将 POST 请求的响应手动写入缓存,以请求 body 的 SHA-256 hash 作为缓存 key:
// 1. 计算缓存 key
const bodyText = JSON.stringify(body);
const hash = await sha256(bodyText);
const cacheUrl = new URL(request.url);
cacheUrl.pathname = "/analytics/" + hash;
const cacheKey = new Request(cacheUrl.toString(), { method: "GET" });
// 2. 尝试命中缓存
let cached = await cache.match(cacheKey);
if (cached) {
const data = await cached.json();
data._cache = "hit"; // 调试标记
return new Response(JSON.stringify(data), { ... });
}
// 3. ... 获取数据 ...
// 4. 异步写入缓存(不阻塞响应)
const cacheResponse = new Response(response.clone().body, {
headers: {
"Cache-Control": "public, max-age=3600",
...
},
});
ctx.waitUntil(cache.put(cacheKey, cacheResponse));
设计要点:
| 做法 | 原因 |
|---|---|
用 GET 请求作为缓存 key |
Cache API 只对 GET 请求生效 |
ctx.waitUntil 写入 |
不阻塞主响应,用户拿到数据时缓存可能还没写完,但下次一定命中 |
max-age=3600 |
1 小时过期,流量数据不需要秒级更新 |
_cache: "hit"/"miss" |
调试时一眼看出是否命中缓存 |
SHA-256 辅助函数
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
return [...new Uint8Array(hashBuffer)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
利用 Cloudflare Workers 内置的 Web Crypto API,无需引入任何依赖。
五、数据处理管线:从原始 API 到前端可消费的 JSON
原始 Cloudflare GraphQL 返回的字段名和嵌套结构对前端不够友好,需要进行清洗和计算。
5.1 每日数据解析
function parseDailyHourly(json) {
const zones = json?.data?.viewer?.zones?.[0];
if (!zones) throw new Error("No data returned from CF API");
const totals = { requests: 0, uniques: 0, pageViews: 0, cachedRequests: 0, bytes: 0, cachedBytes: 0 };
const daily = (zones.daily || []).map((d) => {
const req = d.sum.requests || 0;
const pv = d.sum.pageViews || 0;
const bw = d.sum.bytes || 0;
const cached = d.sum.cachedRequests || 0;
const cachedBytes = d.sum.cachedBytes || 0;
totals.requests += req;
totals.uniques += d.uniq.uniques || 0;
totals.pageViews += pv;
totals.cachedRequests += cached;
totals.bytes += bw;
totals.cachedBytes += cachedBytes;
return {
date: d.dimensions.date,
requests: req,
uniqueVisitors: d.uniq.uniques || 0,
pageViews: pv,
cachedRequests: cached,
cacheHitRate: req ? Math.round((cached / req) * 10000) / 100 : 0,
cacheHitRateBytes: bw ? Math.round((cachedBytes / bw) * 10000) / 100 : 0,
bandwidthBytes: bw,
bandwidthMB: Math.round((bw / 1024 / 1024) * 100) / 100,
};
});
// ...
}
几个值得注意的计算细节:
- 缓存命中率 =
cachedRequests / requests,保留两位小数(乘以 10000 再除以 100) - 带宽换算:bytes → MB,除以 1024 两次
- 除零保护:
req ? ... : 0,避免没有任何请求的日子报错 - 边界情况:
d.sum.requests || 0,某些字段可能为null,用|| 0兜底
5.2 细分数据汇总
30 天的细分数据通过 Map 累加:
function accumulateGroups(z, maps) {
for (const key of ["byCountry", "byDevice", "byBrowser", "byOS", "byCache", "byHTTP"]) {
const groups = z[key] || [];
for (const item of groups) {
const name = item.dimensions[dimKey] || "Unknown";
maps[key].set(name, (maps[key].get(name) || 0) + item.count);
}
}
}
accumulateGroups 用 Map 而不是普通对象来累加,原因很简单:Map 的 key 不受对象属性名限制,且 entries() 遍历能保留插入顺序。
汇总完成后,toBreakdown 将 Map 转换为前端友好的数组格式,并计算百分比:
function toBreakdown(map) {
const total = [...map.values()].reduce((s, v) => s + v, 0);
return [...map.entries()]
.sort((a, b) => b[1] - a[1])
.map(([name, value]) => ({
name,
value,
pct: total ? Math.round((value / total) * 1000) / 10 : 0,
}));
}
5.3 最终返回的数据结构
{
"daily": [{ "date": "2026-06-09", "requests": 9830, "uniqueVisitors": 190, "pageViews": 1087, "cacheHitRate": 45.23, "bandwidthMB": 156.78 }, ...],
"hourly": [{ "datetime": "2026-06-10T00:00:00Z", ... }, ...],
"totals": { "requests": 14612, "uniqueVisitors": 373, "pageViews": 1526, "cacheHitRate": 48.5, "bandwidthMB": 234.56 },
"byCountry": [{ "name": "United States", "value": 5846, "pct": 40.0 }, ...],
"byDevice": [{ "name": "desktop", "value": 7312, "pct": 50.1 }, ...],
"byBrowser": [{ "name": "Chrome", "value": 4235, "pct": 29.0 }, ...],
"byOS": [{ "name": "Windows", "value": 3512, "pct": 24.0 }, ...],
"byCacheStatus": [{ "name": "hit", "value": 7080, "pct": 48.5 }, ...],
"byHTTPProtocol": [{ "name": "HTTP/2", "value": 8900, "pct": 60.9 }, ...],
"generatedAt": "2026-06-10T12:00:00.000Z",
"_cache": "miss"
}
前端拿到这个数据结构后,可以直接传给 ECharts 或 Chart.js 渲染——daily 给折线图,byCountry 给地图或饼图,byDevice / byBrowser / byOS 给饼图,byCacheStatus 给缓存效率面板。
六、CORS 与路由设计
Worker 需要同时处理三种请求:
async fetch(request, env, ctx) {
const url = new URL(request.url);
const origin = request.headers.get("Origin") || "*";
// 1. OPTIONS 预检
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders(origin) });
}
// 2. Ping 健康检查
if (request.method === "GET" && url.pathname === "/ping") {
return new Response(JSON.stringify({ status: "ok", message: "Worker is alive" }), { ... });
}
// 3. POST 获取数据
if (request.method === "POST") {
// ... 核心逻辑 ...
}
return new Response("Not Found", { status: 404 });
}
CORS 头通过动态 Origin 处理:
function corsHeaders(origin) {
return {
"Access-Control-Allow-Origin": origin || "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
}
Origin 来自请求头,而不是硬编码——这样你的 Worker 可以服务于多个前端域名而无需修改代码。
七、部署与配置
7.1 创建 Secret
为了安全,API Token 不要放在 wrangler.json 的 vars 中,而是通过 wrangler secret 加密存储:
wrangler secret put CF_API_TOKEN
# 然后粘贴你的 Token
wrangler secret put CF_ZONE_ID
7.2 部署
wrangler deploy
7.3 配置自定义域名
在 Cloudflare Dashboard 中给 Worker 绑定一个子域名,例如 analytics.lxpavilion.top。这样前端就可以通过这个域名访问 API。
八、前端对接
我博客的前端通过一个简单的 fetch 调用 Worker,完整的类型定义和调用代码:
TypeScript 类型定义
export interface DailyData {
date: string;
requests: number;
uniqueVisitors: number;
pageViews: number;
cachedRequests: number;
cacheHitRate: number;
bandwidthMB: number;
}
export interface BreakdownItem {
name: string;
value: number;
pct: number;
}
export interface AnalyticsData {
daily: DailyData[];
hourly: HourlyData[];
totals: { requests: number; uniqueVisitors: number; pageViews: number; cacheHitRate: number; bandwidthMB: number };
byCountry: BreakdownItem[];
byDevice: BreakdownItem[];
byBrowser: BreakdownItem[];
byOS: BreakdownItem[];
byCacheStatus: BreakdownItem[];
byHTTPProtocol: BreakdownItem[];
generatedAt: string;
}
调用函数
const WORKER_URL = "https://analytics.lxpavilion.top";
export async function fetchAnalytics(days: number = 7): Promise<AnalyticsData> {
const resp = await fetch(WORKER_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ days: Math.min(days, 364) }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error((err as any).error || `API error: ${resp.status}`);
}
return resp.json();
}
调用方式非常简单:
const data = await fetchAnalytics(7);
// data.daily -> 折线图数据
// data.byCountry -> 国家分布饼图
// data.byDevice -> 设备分布饼图
// data.totals.cacheHitRate -> 缓存效率指标卡
九、效果展示
以下是我博客中数据分析页面的实际效果截图。



数据看板展示近 7 天的请求趋势、访客变化以及缓存效率等关键指标,你可以在 lxpavilion.top/analytics 查看完整页面。
十、总结与设计思路回顾
整个 Worker 的设计围绕三个核心目标展开:
| 目标 | 实现方式 |
|---|---|
| 数据全面 | 一次请求出日级 + 小时级数据,六维度细分数据逐天拉取 |
| 性能可控 | pMapSerial 5 路并发,SHA-256 缓存 key + 1 小时 TTL |
| 前端友好 | 返回结构化 JSON,字段名直接对应图表需求 |
对比 Hello World 级别的 Worker,这份脚本多出的核心工程考量:
- 容错设计:单天数据拉取失败不阻塞整体,
|| 0防null,除零保护 - 缓存穿透防护:缓存 miss 时才请求 CF API,
ctx.waitUntil异步写缓存 - 边界处理:
Math.min(days, 364)防止超范围查询,Array.from生成精确 30 天日期序列 - 调试友好:
_cache标记标明 hit/miss,generatedAt记录生成时间
完整脚本
完整的 cf-analytics-worker.js 约 270 行已上传至GitHub Gist / 仓库,所有代码已部署在 analytics.lxpavilion.top,前端效果可见 lxpavilion.top/analytics。
相关阅读:
- 给我的博客添加监控板块:使用 Cloudflare API 获取网站流量信息(入门篇)
- Cloudflare GraphQL Analytics API 官方文档
- Workers Cache API 文档
📝 本文发布于 栏轩阁
🌐 欢迎关注我的其他平台:
📧 联系我:2194844980@qq.com

浙公网安备 33010602011771号