居然一年没更新博客了… ( ̄ ‘i  ̄;) 博主最近一年比较忙,但确实还活着
![图片[1]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122144041993.png)
引言
最近得知 Umami 推出了 V3 版本,又听说 V2 版本有严重漏洞 ,遂决定花点时间升级 Umami,并重写一下博客底部 Umami 数据挂件的代码。
![图片[2]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122144309847-618x800.png)
刚要开工,发现 Umami V3 居然不再支持 MySQL 了,必须使用 PostgreSQL 作为数据库…(气)花费了一晚上的时间,跟着官方文档《Migrate MySQL to PostgreSQL|将 MySQL 迁移至 PostgreSQL》折腾,可惜最后还是因为众多奇奇怪怪的小问题未能够成功迁移,只好转向全新部署。
![图片[3]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122150301486-1024x672.png)
本文记录了把 Umami 升级到 V3 版本后,根据官方文档重写数据挂件的过程,希望对其他使用 Umami 有相关需求的博主有所帮助。
为什么要重写?
Umami V3 的 API 有一定改动,主要体现在:
- API 响应格式变了:
V2 返回的是:
{
uniques: {value: 123},
pageviews: {value: 456}
}
V3 直接简化成了:
{
visitors: 123,
pageviews: 456
}
- 端点地址调整:虽然路径差不多,但参数和返回值都有变化
- 认证方式不变:还好 Bearer Token 认证方式没变,不然更麻烦
实现的功能
![图片[4]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122144041993.png)
这个挂件很简单,就是在博客底部显示四个关键数据:
- 累计 PV:从建站到现在的总页面浏览量
- 累计 UV:从建站到现在的独立访客数
- 当前在线人数:最近 5 分钟内的活跃访客
- 今日访客数:今天有多少人访问过
![图片[5]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122153502359.png)
另外,之前的数据因为全新部署丢失了,所以我的代码里加了个 ”历史数据补偿“ 的功能(见上图),不需要可注释掉,或者直接在相关脚本处填 0。
1. 准备工作:在 Umami 创建只读用户
为了安全考虑,我们不直接用管理员账号的 Token,而是创建一个只读权限的用户。这样即使 Token 泄露,别人也没法修改你的统计数据。
第一步:创建团队
- 登录你的 Umami 后台(管理员账号)
- 点击左侧菜单的 Teams
- 点击右上角 Create team 按钮
- 填写团队名称,比如
Blog Viewer - 点击 Save 保存
- 保存后会看到团队列表,点击刚创建的团队进入详情页
- 找到 Access code(加入团队的编码),复制保存这个编码,一会儿要用
第二步:添加网站到团队
- 还是在刚才创建的团队详情页
- 找到 Websites 区域,点击 Add website
- 选择你要统计的网站(比如你的博客网站)
- 点击 Save
现在这个团队就可以访问你的网站统计数据了。
第三步:创建只读用户
- 回到左侧菜单,点击 Settings → Users
- 点击 Create user 按钮
- 填写用户信息:
- Username:随便起个名字,比如
blog_viewer - Password:设置一个密码,记住它
- Role:选择 User(普通用户,不是 Admin)
- Username:随便起个名字,比如
- 点击 Save 保存
第四步:让新用户加入团队
- 打开浏览器无痕窗口,在无痕窗口中访问你的 Umami 登录页面
- 使用刚才创建的
blog_viewer账号登录 - 登录后会看到一个空白页面或提示,找到 Teams 入口
- 点击 Join team 或类似按钮
- 输入刚才复制的 Access code(团队加入编码)
- 确认加入
现在这个 blog_viewer 用户就可以查看你博客的统计数据了,但没有修改权限。
第五步:获取 API Token
![图片[6]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122153626563.png)
继续在无痕窗口中操作(保持 blog_viewer 账号登录状态):
- 按 F12 打开浏览器开发者工具
- 切换到 Console(控制台)标签页
- 复制下面的代码,替换其中的域名、用户名和密码:
fetch('https://你的umami域名/api/auth/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: 'blog_viewer', // 刚才创建的用户名
password: '你设置的密码'
})
}).then(r => r.json()).then(d => console.log('Token:', d.token))
- 粘贴到控制台,按 回车 执行
- 会看到控制台输出
Token: eQTMjUzIiwiY...这样一长串字符 - 复制这个 Token(不包括
Token:这几个字),保存好
注意:这个 Token 会一直有效,除非你修改了密码或者 Umami 服务器重启。所以获取一次就够了。
第六步:获取网站 ID
还是在无痕窗口(blog_viewer 登录状态):
- 在 Umami 后台点击左侧的 Websites
- 找到你的网站,点击 View 或网站名称进入统计页面
- 看浏览器地址栏,网址格式类似:
https://你的域名/websites/a6541980-4e87-4633-8eb9-0e6774b9760e - 最后那一串就是 Website ID(
a6541980-4e87-4633-8eb9-0e6774b9760e),复制保存
到这里,准备工作就完成了。你应该有三样东西:
- ✅ API Token(一长串字符)
- ✅ Website ID(UUID 格式)
- ✅ 你的 Umami 服务器域名
2. 在网站中配置代码
把下面的代码复制到你的博客模板文件中:
<script>
(function() {
'use strict';
const CONFIG = {
websiteId: '你的Website ID', // 第六步获取的
apiBase: 'https://你的umami域名', // 比如 https://analytics-v3.baiyuyu.com
token: '第五步获取的Token', // 那一长串字符
startDate: '2023-01-01', // 统计开始日期(从哪一天开始获取数据)
legacyData: {
totalUV: 0, // 博主加的数据补偿功能,如果之前有旧数据,填在这里(没有就填 0)
totalPV: 0
},
selectors: {
totalPV: '#total-pv',
totalUV: '#total-uv',
onlineUser: '#online-user',
todayUV: '#today-uv'
},
cache: {
enabled: true,
duration: 60000
}
};
const cache = new Map();
function getTodayRange() {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
return { start, end };
}
async function fetchUmamiAPI(endpoint, params = {}) {
const cacheKey = `${endpoint}-${JSON.stringify(params)}`;
if (CONFIG.cache.enabled) {
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.time < CONFIG.cache.duration) {
return cached.data;
}
}
const url = new URL(`${CONFIG.apiBase}/api/websites/${CONFIG.websiteId}${endpoint}`);
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null) {
url.searchParams.append(k, v);
}
});
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.token}`
}
});
if (!response.ok) {
throw new Error(`API 请求失败: ${response.status}`);
}
const data = await response.json();
if (CONFIG.cache.enabled) {
cache.set(cacheKey, { data, time: Date.now() });
}
return data;
} catch (error) {
console.error(`Umami API 错误 (${endpoint}):`, error);
throw error;
}
}
function updateElement(selector, value) {
const el = document.querySelector(selector);
if (el) el.textContent = value;
}
async function fetchTotalStats() {
try {
const startTime = new Date(CONFIG.startDate).getTime();
const endTime = Date.now();
const data = await fetchUmamiAPI('/stats', { startAt: startTime, endAt: endTime });
const totalPV = (data.pageviews || 0) + CONFIG.legacyData.totalPV;
const totalUV = (data.visitors || 0) + CONFIG.legacyData.totalUV;
updateElement(CONFIG.selectors.totalPV, totalPV.toLocaleString());
updateElement(CONFIG.selectors.totalUV, totalUV.toLocaleString());
} catch (error) {
console.error('获取累计统计失败:', error);
updateElement(CONFIG.selectors.totalPV, 'Error');
updateElement(CONFIG.selectors.totalUV, 'Error');
}
}
async function fetchOnlineUsers() {
try {
const data = await fetchUmamiAPI('/active');
const count = data.visitors || 0;
updateElement(CONFIG.selectors.onlineUser, count);
} catch (error) {
console.error('获取在线用户失败:', error);
updateElement(CONFIG.selectors.onlineUser, 'Error');
}
}
async function fetchTodayStats() {
try {
const { start, end } = getTodayRange();
const data = await fetchUmamiAPI('/stats', { startAt: start, endAt: end });
const visitors = data.visitors || 0;
updateElement(CONFIG.selectors.todayUV, visitors.toLocaleString());
} catch (error) {
console.error('获取今日统计失败:', error);
updateElement(CONFIG.selectors.todayUV, 'Error');
}
}
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return func.apply(this, args);
}
};
}
async function initStats() {
await Promise.allSettled([
fetchTotalStats(),
fetchOnlineUsers(),
fetchTodayStats()
]);
}
const throttledRefresh = throttle(() => {
fetchOnlineUsers();
fetchTodayStats();
}, 30000);
function enableAutoRefresh() {
setInterval(throttledRefresh, 30000);
setInterval(fetchTotalStats, 300000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initStats);
} else {
initStats();
}
enableAutoRefresh();
})();
</script>
配置说明:
- websiteId:填入第六步获取的 Website ID
- apiBase:填入你的 Umami 域名(要加
https://) - token:填入第五步获取的 Token
- startDate:改成你博客的建站日期
- legacyData:如果你之前有统计数据但换了系统,可以在这里补上旧数据;没有的话就保持
0
3. 添加 HTML 显示元素
在你博客模板中想显示统计数据的位置(比如页脚),加上这些 HTML 代码:
<div class="stats-widget">
<p>
📊 累计访问:<strong id="total-pv">加载中...</strong> PV / <strong id="total-uv">加载中...</strong> UV
</p>
<p>
👥 在线人数:<strong id="online-user">加载中...</strong> 人
</p>
<p>
🎉 今日第 <strong id="today-uv">加载中...</strong> 位访客
</p>
</div>
你可以根据自己的喜好调整样式和布局。重要的是保持这四个 ID:
#total-pv– 累计 PV#total-uv– 累计 UV#online-user– 在线人数#today-uv– 今日访客
4. 验证是否成功
保存代码后,刷新你的博客页面:
- 打开浏览器开发者工具(F12)
- 切换到 Console 标签
- 看有没有报错信息
- 检查页面上的数据是否正常显示
如果看到类似这样的输出,说明成功了:
使用缓存数据: /stats
使用缓存数据: /active
使用缓存数据: /stats
如果看到 401 Unauthorized 错误,检查:
- Token 是否正确复制(没有多余空格)
- Website ID 是否正确
- 域名是否正确(要加 https://)
如果看到 404 Not Found 错误,检查:
- Website ID 是否正确
- 该用户是否有权限查看这个网站的数据
代码解析
缓存机制
cache: {
enabled: true,
duration: 60000 // 60秒 = 1分钟
}
同样的数据 1 分钟内会直接用缓存,不会重复请求 API。这样既省流量又提速,还能减轻 Umami 服务器压力。
自动刷新
setInterval(throttledRefresh, 30000); // 在线人数和今日访客,30秒刷新
setInterval(fetchTotalStats, 300000); // 累计数据,5分钟刷新
不同类型的数据刷新频率不同:
- 在线人数、今日访客:变化快,30 秒刷新一次
- 累计数据:变化慢,5 分钟刷新一次就够了
节流控制
const throttledRefresh = throttle(() => {
fetchOnlineUsers();
fetchTodayStats();
}, 30000);
即使刷新函数被多次触发,节流机制也会保证最快 30 秒才执行一次,避免高频请求。
历史数据补偿
const totalPV = (data.pageviews || 0) + CONFIG.legacyData.totalPV;
const totalUV = (data.visitors || 0) + CONFIG.legacyData.totalUV;
如果你之前有统计数据但换了新系统,可以把旧数据填在 legacyData 里,代码会自动累加。比如我之前有 43100 UV 和 34800 PV,就这样配置:
legacyData: {
totalUV: 43100,
totalPV: 34800
}
性能问题
有人可能担心累计访问量查询会不会很慢。实测下来,Umami 的聚合查询还是挺快的,基本都在 100ms 以内。而且有了缓存机制,即使访问量到了几十万,性能也完全没问题。
如果你的数据量特别大(百万级以上),可以考虑把刷新间隔调大一点:
setInterval(fetchTotalStats, 600000); // 改成 10 分钟刷新一次
常见问题
Q1:Token 会过期吗?
不会。Token 会一直有效,除非:
- 你修改了该用户的密码
- Umami 服务器重启(某些配置下)
所以获取一次就够了,不需要每次都重新获取。
Q2:可以在多个网站用同一个脚本吗?
可以,但要注意修改 websiteId。每个网站的 ID 不同,需要单独配置。
Q3:显示 “Error” 怎么办?
打开浏览器控制台(F12),看具体的错误信息:
401错误:Token 或权限问题404错误:Website ID 错误或用户没有权限Network Error:检查 Umami 服务器是否正常运行
总结
![图片[7]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据](https://cdn.baiyuyu.com/2026/01/20260122143108784-1024x757.png)
Umami V3 的 API 设计比 V2 简洁,响应格式也更直观。虽然升级需要改代码,但改完之后维护起来反而更方便。通过创建只读用户,可以安全地在博客上展示统计数据,不用担心 Token 泄露的问题。
整个流程下来可能需要 10-15 分钟,但设置一次就能一直用,还是很值得的。这个挂件脚本我自己用了几天,目前似乎运行稳定,若遇到问题欢迎留言讨论~
相关资源




