升级到 Umami V3 ,重写博客底部的 “数据挂件” 脚本,显示 UV/PV 等访问数据

居然一年没更新博客了… ( ̄ ‘i  ̄;) 博主最近一年比较忙,但确实还活着

图片[1]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
数据挂件示例

引言

最近得知 Umami 推出了 V3 版本,又听说 V2 版本有严重漏洞 ,遂决定花点时间升级 Umami,并重写一下博客底部 Umami 数据挂件的代码。

图片[2]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
Umami V2 受安全漏洞影响

刚要开工,发现 Umami V3 居然不再支持 MySQL 了,必须使用 PostgreSQL 作为数据库…(气)花费了一晚上的时间,跟着官方文档《Migrate MySQL to PostgreSQL|将 MySQL 迁移至 PostgreSQL》折腾,可惜最后还是因为众多奇奇怪怪的小问题未能够成功迁移,只好转向全新部署。

图片[3]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
Migrate MySQL to PostgreSQL|将 MySQL 迁移至 PostgreSQL 文档

本文记录了把 Umami 升级到 V3 版本后,根据官方文档重写数据挂件的过程,希望对其他使用 Umami 有相关需求的博主有所帮助。

为什么要重写?

Umami V3 的 API 有一定改动,主要体现在:

  1. API 响应格式变了

V2 返回的是:

{
    uniques: {value: 123},
    pageviews: {value: 456}
}

V3 直接简化成了:

{
    visitors: 123,
    pageviews: 456
}
  1. 端点地址调整:虽然路径差不多,但参数和返回值都有变化
  2. 认证方式不变:还好 Bearer Token 认证方式没变,不然更麻烦

实现的功能

图片[4]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
数据挂件效果示例

这个挂件很简单,就是在博客底部显示四个关键数据:

  • 累计 PV:从建站到现在的总页面浏览量
  • 累计 UV:从建站到现在的独立访客数
  • 当前在线人数:最近 5 分钟内的活跃访客
  • 今日访客数:今天有多少人访问过
图片[5]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
数据补偿功能

另外,之前的数据因为全新部署丢失了,所以我的代码里加了个 ”历史数据补偿“ 的功能(见上图),不需要可注释掉,或者直接在相关脚本处填 0。

 

1. 准备工作:在 Umami 创建只读用户

为了安全考虑,我们不直接用管理员账号的 Token,而是创建一个只读权限的用户。这样即使 Token 泄露,别人也没法修改你的统计数据。

第一步:创建团队

  1. 登录你的 Umami 后台(管理员账号)
  2. 点击左侧菜单的 Teams
  3. 点击右上角 Create team 按钮
  4. 填写团队名称,比如 Blog Viewer
  5. 点击 Save 保存
  6. 保存后会看到团队列表,点击刚创建的团队进入详情页
  7. 找到 Access code(加入团队的编码),复制保存这个编码,一会儿要用

第二步:添加网站到团队

  1. 还是在刚才创建的团队详情页
  2. 找到 Websites 区域,点击 Add website
  3. 选择你要统计的网站(比如你的博客网站)
  4. 点击 Save

现在这个团队就可以访问你的网站统计数据了。

第三步:创建只读用户

  1. 回到左侧菜单,点击 SettingsUsers
  2. 点击 Create user 按钮
  3. 填写用户信息:
    • Username:随便起个名字,比如 blog_viewer
    • Password:设置一个密码,记住它
    • Role:选择 User(普通用户,不是 Admin)
  4. 点击 Save 保存

第四步:让新用户加入团队

  1. 打开浏览器无痕窗口,在无痕窗口中访问你的 Umami 登录页面
  2. 使用刚才创建的 blog_viewer 账号登录
  3. 登录后会看到一个空白页面或提示,找到 Teams 入口
  4. 点击 Join team 或类似按钮
  5. 输入刚才复制的 Access code(团队加入编码)
  6. 确认加入

现在这个 blog_viewer 用户就可以查看你博客的统计数据了,但没有修改权限。

第五步:获取 API Token

图片[6]|升级到 Umami V3 并重写博客底部的“数据挂件”脚本,显示博客 UV/PV 访问数据
浏览器控制台

继续在无痕窗口中操作(保持 blog_viewer 账号登录状态):

  1. F12 打开浏览器开发者工具
  2. 切换到 Console(控制台)标签页
  3. 复制下面的代码,替换其中的域名、用户名和密码
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))
  1. 粘贴到控制台,按 回车 执行
  2. 会看到控制台输出 Token: eQTMjUzIiwiY... 这样一长串字符
  3. 复制这个 Token(不包括 Token: 这几个字),保存好

注意:这个 Token 会一直有效,除非你修改了密码或者 Umami 服务器重启。所以获取一次就够了。

第六步:获取网站 ID

还是在无痕窗口(blog_viewer 登录状态):

  1. 在 Umami 后台点击左侧的 Websites
  2. 找到你的网站,点击 View 或网站名称进入统计页面
  3. 看浏览器地址栏,网址格式类似:https://你的域名/websites/a6541980-4e87-4633-8eb9-0e6774b9760e
  4. 最后那一串就是 Website IDa6541980-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>

配置说明:

  1. websiteId:填入第六步获取的 Website ID
  2. apiBase:填入你的 Umami 域名(要加 https://
  3. token:填入第五步获取的 Token
  4. startDate:改成你博客的建站日期
  5. 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. 验证是否成功

保存代码后,刷新你的博客页面:

  1. 打开浏览器开发者工具(F12)
  2. 切换到 Console 标签
  3. 看有没有报错信息
  4. 检查页面上的数据是否正常显示

如果看到类似这样的输出,说明成功了:

使用缓存数据: /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 访问数据
Umami V3

Umami V3 的 API 设计比 V2 简洁,响应格式也更直观。虽然升级需要改代码,但改完之后维护起来反而更方便。通过创建只读用户,可以安全地在博客上展示统计数据,不用担心 Token 泄露的问题。

整个流程下来可能需要 10-15 分钟,但设置一次就能一直用,还是很值得的。这个挂件脚本我自己用了几天,目前似乎运行稳定,若遇到问题欢迎留言讨论~

 

相关资源


全文完

有用0阅读 40版权提示
留言 共 1 条
其实你有必要说两句
匿名的头像|白鱼小栈

昵称

有回复时发送邮件通知我

取消
身份 表情 代码 图片