上周五晚上,我盯着博客后台那个“总访问量:128”的数字发呆。这破站搭了快半年,除了我自己点来看格式对不对,估计就剩爬虫光顾了。不行,得加点东西——至少让我知道谁在什么时候点进来的,哪个页面最受欢迎。
需求很简单:统计每天的独立访客数(UV)、页面浏览量(PV)、还有热门文章排行。不搞复杂,不上第三方平台(Google Analytics太重,百度统计又嫌它扒数据太狠),自己写个轻量级的。
技术选型这块没纠结太久。博客是静态站,托管在Vercel上,用Next.js搭的。要加统计,最直接的办法就是自己搭个后端API,前端发请求记录数据,后端存数据库。
脑子一热选了Supabase。免费额度够用,PostgreSQL数据库,REST API直接调,省得自己写后端路由。注册账号、建项目、拿API密钥,十分钟搞定。
表结构设计得很简单:
“`sql
CREATE TABLE page_views (
id BIGSERIAL PRIMARY KEY,
path TEXT NOT NULL,
visitor_id TEXT NOT NULL,
user_agent TEXT,
referrer TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_created_at ON page_views(created_at);
CREATE INDEX idx_path ON page_views(path);
“`
visitor_id 用浏览器的指纹生成,不是cookie——因为我不想弹那个烦人的“本网站使用cookie”的横幅。fingerprintjs 这个库挺好用,40KB gzip完大概12KB,能接受。
前端代码就一个API调用,放在 _app.tsx 的 useEffect 里:
“`typescript
useEffect(() => {
const fp = await FingerprintJS.load();
const result = await fp.get();
fetch(‘/api/track’, {
method: ‘POST’,
body: JSON.stringify({
path: window.location.pathname,
visitorId: result.visitorId,
userAgent: navigator.userAgent,
referrer: document.referrer
})
});
}, []);
“`
后端用Vercel Serverless Functions,一个文件搞定:
“`typescript
// pages/api/track.ts
import { createClient } from ‘@supabase/supabase-js’;
export default async function handler(req, res) {
if (req.method !== ‘POST’) {
return res.status(405).json({ error: ‘Method not allowed’ });
}
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
);
const { error } = await supabase
.from(‘page_views’)
.insert([req.body]);
if (error) {
console.error(‘Supabase insert error:’, error);
return res.status(500).json({ error: ‘Failed to track’ });
}
res.status(200).json({ success: true });
}
“`
看起来完美,部署上去,打开博客,F12看网络请求——200。数据表里也插进去了。成了!
第二天早上起来一看数据,好家伙,一晚上多了3000多条记录。我博客日活撑死30个人,哪来的3000?查了查IP来源,全是Vercel的CDN节点。明白了——每次部署、每次预渲染、甚至每次Vercel的健康检查,都会触发这个API调用。
这就是第一个坑:**Serverless Function在Vercel上会被各种内部服务调用**。解决方案是加一个简单的来源校验:
“`typescript
const userAgent = req.body.userAgent || ”;
if (userAgent.includes(‘Vercel’) || userAgent.includes(‘bot’)) {
return res.status(204).end();
}
“`
但光这样不够,因为有些爬虫会伪造UA。我又加了一层——检查请求来源IP,如果是Vercel内部IP段直接跳过。虽然不完美,但能过滤掉大部分无效请求。
第二个坑来了。FingerprintJS这个库,在iOS Safari上有个bug——有时候拿不到指纹,直接抛异常。导致整个页面白屏。查了半天,发现是Safari的智能防跟踪(ITP)搞的鬼。解决办法是把指纹生成逻辑包在try-catch里,失败了就生成一个随机UUID兜底:
“`typescript
let visitorId;
try {
const fp = await FingerprintJS.load();
const result = await fp.get();
visitorId = result.visitorId;
} catch (e) {
visitorId = crypto.randomUUID();
console.warn(‘FingerprintJS failed, using fallback:’, e);
}
“`
第三个坑比较隐蔽。我发现同一个人访问同一个页面,visitorId居然变了。排查了半天,发现是FingerprintJS在隐身模式下会重置指纹——因为隐身模式不持久化任何数据。这个无解,只能接受。统计里会有少量重复计数,但误差在可接受范围内。
第四个坑……算了,一个个数太累,直接说最疼的几个。
数据量上去之后,Supabase的查询开始变慢。热门文章排行这个功能,需要group by path然后count,表里才几万条数据,查询就要2秒多。加索引也没用,因为count(*)是全表扫描。解决方案是建了个物化视图:
“`sql
CREATE MATERIALIZED VIEW daily_stats AS
SELECT
date_trunc(‘day’, created_at) as day,
path,
COUNT(DISTINCT visitor_id) as uv,
COUNT(*) as pv
FROM page_views
GROUP BY day, path;
“`
每天凌晨跑一次刷新。实时性要求不高的统计,用物化视图香得很。
第五个坑,Supabase的免费版有请求次数限制。我博客虽然流量小,但每个页面加载都发一次POST请求,一个月下来轻松超过免费额度。优化方案是前端加了个节流——同一个visitor在30秒内多次访问同一个path,只记录一次。用localStorage存上次访问时间戳:
“`typescript
const lastVisit = localStorage.getItem(`last_visit_${path}`);
const now = Date.now();
if (lastVisit && (now – parseInt(lastVisit)) < 30000) {
return; // 30秒内重复访问不记录
}
localStorage.setItem(`last_visit_${path}`, now.toString());
“`
这样把请求量降到了原来的十分之一。
第六个坑,Vercel Serverless Function有10秒超时限制。虽然我的API一般几十毫秒就返回了,但有一次Supabase那边抽风,响应延迟到了15秒,前端直接报504。加了个超时兜底:
“`typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
fetch(‘/api/track’, {
method: ‘POST’,
signal: controller.signal,
// …
}).finally(() => clearTimeout(timeout));
“`
超时了也不影响页面展示,统计丢了就丢了,不能因为统计功能拖慢主站。
第七个坑……算了,直接跳到总结。这11个坑踩下来,最深的体会就几点:
一是**不要高估第三方服务的稳定性**。Supabase再牛逼,也是别人家的服务,该抽风还是抽风。关键路径上必须有降级方案。
二是**统计功能看着简单,水很深**。去重、防刷、隐私合规、性能开销……每个点都能折腾半天。如果流量再大点,还得上消息队列、异步写入、缓存预热。我这小站暂时够用了。
三是**能用现成的就别自己写**。要不是为了好玩,直接上Plausible或者Umami这种自部署的统计工具,半小时就搞定。自己写虽然踩坑多,但数据库在自己手里,数据想怎么查就怎么查——比如我现在能查“哪个操作系统的人最常看我的博客”,答案是macOS占60%,Windows占30%,Linux占10%。冷知识:居然还有3个访客用Windows Phone。
四是**加功能之前先想想值不值**。这个统计功能花了我三个周末,换来的是每天盯着“UV: 12”这个数字焦虑。早知道这样,不如把这时间拿来多写两篇文章。
最后留个彩蛋。我在数据库里查到了自己博客被爬虫光顾的记录——有个IP在凌晨3点访问了所有页面,每次间隔正好5秒,UA是空字符串。不用想都知道是谁在偷数据。不过算了,我这破站也没什么值得偷的,爬就爬吧。
📎 延伸阅读
看完这篇,如果你想:
- 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
- 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现