用 Umami 给网站做一个前台实时统计挂件:在线人数、今日 / 昨日 / 本周 / 本月、环比涨跌等(免 API Key)

前言

Umami 是一款隐私友好的开源网站统计工具,但它的后台默认是给站长自己看的,访客在前台看不到数据。本文分享一个不依赖任何框架、不依赖特定建站程序的纯文字统计挂件:只要是能放 HTML 的网站(静态页、WordPress、Typecho、Halo、各种博客系统都行),一段代码就能在前台显示实时流量,还能自动计算「较昨日 / 上周 / 上月」的环比涨跌。

一、最终效果

在页脚、侧边栏或任意页面放一行文字,自动显示:

主要特点:

  • 纯文字、无边框无卡片:字号、颜色、字体自动继承你当前主题,放进任何页面都不违和;
  • 实时自动刷新:在线人数每 5 秒、其余数据每 10 秒更新,频率可自行调整;
  • 安全只读:使用 Umami 的「分享链接」取数,不需要在前端放 API Key,随时可在后台关闭;
  • 自由增删:想显示哪几条、加哪些指标,只改 HTML 里一行文字即可,不用碰 JS;
  • 自带环比涨跌:今日对比昨日同时段、本周对比上周、本月对比上月,涨绿跌红。

二、准备工作(约 1 分钟)

  1. 确认你的网站已经接入 Umami 统计(页面里带有 data-website-id 的统计脚本)。
  2. 登录 Umami 后台,进入该网站的 设置 / Websites,找到 分享(Share),点 添加,生成一条分享链接。
  3. 复制这条分享链接,形如 https://你的域名/share/xxxxxxxxxxxx,代码里要用到。

原理:挂件先用这条分享链接换取一个只读令牌,再用它读取统计数据。该令牌只能看到你在分享设置里允许的内容,不含账号管理权限,也随时可以关闭。

三、完整代码(已逐条加注释)

把下面这一整段(style + div + script)复制出来,务必把 data-share-url 改成你自己的分享链接:

<!-- ========== Umami 纯文字实时统计 · 开始 ========== -->
<!-- 用法:把这一整段(style + div + script)放进网页任意位置即可。 -->
<!-- 换网站:只改下面 div 里的 data-share-url。 -->
<!-- 增删条目:只改 <div id="umami-stats-text"> 里的 <span>,JS 不用动。 -->

<style>
/* 外层容器:字号、字体、颜色默认继承你主题本身,和周围文字风格一致 */
#umami-stats-text{
  font-family: inherit;     /* 字体:继承主题(想指定可改成如 "Microsoft YaHei") */
  font-size: 14px;          /* 文字大小,自行改大改小 */
  line-height: 2;           /* 行距(窄屏自动换行时更好看) */
  color: inherit;           /* 文字颜色:继承主题 */
}
/* 每一条统计作为一个整体,不允许从中间断行;窄屏会在条目之间换行 */
#umami-stats-text .ust-item{
  display: inline-block;
  white-space: nowrap;
}
/* 在相邻两条之间自动插入一个小圆点“·”作为分隔符 */
#umami-stats-text .ust-item + .ust-item::before{
  content: "·";
  margin: 0 12px;           /* 分隔圆点左右间距 */
  color: #9aa1af;           /* 分隔圆点颜色 */
}
/* 数字:加粗 + 等宽数字,数字跳动时不左右晃动 */
#umami-stats-text b{
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* 环比涨跌的小字前缀(“较昨日/较上周/较上月”) */
#umami-stats-text .ust-trend-label{
  font-size: 12px;
  color: #9aa1af;
  margin-left: 2px;
}
/* 环比涨跌数字本体 */
#umami-stats-text .ust-trend{
  font-weight: 600;
  font-size: 12px;
}
/* 涨:绿色;跌:红色;持平/无数据:灰色。若按A股习惯红涨绿跌,把两个颜色对调即可 */
#umami-stats-text .ust-trend.is-up{ color: #16a34a; }
#umami-stats-text .ust-trend.is-down{ color: #dc2626; }
#umami-stats-text .ust-trend.is-flat{ color: #9aa1af; }
</style>

<!-- data-share-url:你的 Umami 分享链接(换别的网站就改这一行) -->
<div id="umami-stats-text" data-share-url="https://umami.lyx6.com/share/xxxxxxxxx">/*这里改成你自己的统计地址*/

  <!-- 每条 = 一个 <span class="ust-item">;<b data-stat="xxx"> 会被自动填数字,
       <span data-trend="xxx"> 会被自动填环比涨跌 -->
  <span class="ust-item">在线 <b data-stat="online">–</b> 人</span>

  <span class="ust-item">今日 <b data-stat="todayPV">–</b> 次 / <b data-stat="todayUV">–</b> 人
    <span class="ust-trend-label">较昨日 <span class="ust-trend" data-trend="todayPV">–</span></span>
  </span>

  <span class="ust-item">昨日 <b data-stat="yesterdayPV">–</b> 次 / <b data-stat="yesterdayUV">–</b> 人</span>

  <span class="ust-item">本周 <b data-stat="weekPV">–</b> 次 / <b data-stat="weekUV">–</b> 人
    <span class="ust-trend-label">较上周 <span class="ust-trend" data-trend="weekPV">–</span></span>
  </span>

  <span class="ust-item">本月 <b data-stat="monthPV">–</b> 次 / <b data-stat="monthUV">–</b> 人
    <span class="ust-trend-label">较上月 <span class="ust-trend" data-trend="monthPV">–</span></span>
  </span>

  <span class="ust-item">累计 <b data-stat="allPV">–</b> 次 / <b data-stat="allUV">–</b> 人</span>

  <!--
    ================= 如何增删条目(只改这里,不用动 JS) =================
    1) 不要哪条:把那一整行 <span class="ust-item">…</span> 删掉即可。
    2) 增加一条:复制一行 <span>,改文字和 data-stat / data-trend 的值即可。
    3) 数字位 data-stat 可填的全部值(后台已自动取好,随用随取):
         online                                                     当前在线人数(最近5分钟活跃)
         todayPV     / todayUV     / todayVisits      今日:访问量 / 访客 / 访问次数
         yesterdayPV / yesterdayUV / yesterdayVisits  昨日:访问量 / 访客 / 访问次数
         weekPV      / weekUV      / weekVisits       本周:访问量 / 访客 / 访问次数
         monthPV     / monthUV     / monthVisits      本月:访问量 / 访客 / 访问次数
         allPV       / allUV       / allVisits        累计:访问量 / 访客 / 访问次数
       (PV=访问量,即页面浏览次数;UV=访客数,即独立访客;Visits=访问次数/会话数)
    4) 环比位 data-trend 可填(自动算涨跌并染色):
         todayPV / todayUV      今日 对比 昨日同时段
         weekPV  / weekUV       本周 对比 上周同时段
         monthPV / monthUV      本月 对比 上月同时段
       举例:今日也想看访客环比,就再加一个 <span class="ust-trend" data-trend="todayUV">–</span>
    ======================================================================
  -->
</div>

<script>
(function () {
  // ===== 1. 找到上面的统计容器,读取它的分享链接 =====
  var root = document.getElementById('umami-stats-text');
  if (!root) return;                          // 页面没放容器就退出,不报错

  // ===== 2. 刷新频率(毫秒,1000=1秒),数值越小越“实时”,可自行调整 =====
  var ACTIVE_INTERVAL = 5000;                 // 在线人数:5 秒(与 Umami 官方实时页一致)
  var STATS_INTERVAL  = 10000;                // 其余统计:10 秒(请求区间较多,建议 10~30 秒)

  // 从分享链接里解析出“接口域名”和“分享标识”
  var m = (root.getAttribute('data-share-url') || '').match(/^(https?:\/\/[^/]+)\/share\/([^/?#]+)/);
  if (!m) return;                             // 链接格式不对就退出
  var apiBase = m[1] + '/api';                // 接口地址,例如 https://你的域名/api
  var slug    = m[2];                         // 分享标识
  var token, wid;                             // token=只读分享令牌;wid=网站ID(自动获取)

  var DAY = 86400000;                         // 一天的毫秒数
  var H8  = 8 * 3600000;                      // 八小时(北京时间=UTC+8)

  // ===== 3. 数字“数据池”:页面上的 data-stat 对应这里的名字,自动填值 =====
  var DATA = {
    online: 0,
    todayPV: 0,     todayUV: 0,     todayVisits: 0,
    yesterdayPV: 0, yesterdayUV: 0, yesterdayVisits: 0,
    prevDayPV: 0,   prevDayUV: 0,   prevDayVisits: 0,    // 昨日同时段(今日环比基准)
    weekPV: 0,      weekUV: 0,      weekVisits: 0,
    prevWeekPV: 0,  prevWeekUV: 0,  prevWeekVisits: 0,   // 上周同时段(本周环比基准)
    monthPV: 0,     monthUV: 0,     monthVisits: 0,
    prevMonthPV: 0, prevMonthUV: 0, prevMonthVisits: 0,  // 上月同时段(本月环比基准)
    allPV: 0,       allUV: 0,       allVisits: 0
  };

  // 环比映射:哪个指标该和谁比
  var TREND_MAP = {
    todayPV: 'prevDayPV',  todayUV: 'prevDayUV',
    weekPV:  'prevWeekPV', weekUV:  'prevWeekUV',
    monthPV: 'prevMonthPV', monthUV: 'prevMonthUV'
  };

  // 数字格式化:加千位分隔符,如 1234 -> 1,234
  function fmt(n) { return Math.round(Number(n) || 0).toLocaleString('en-US'); }
  // 接口有时返回数字、有时返回 {value:数字},两种都兼容
  function val(x) { return (x && typeof x === 'object') ? (x.value || 0) : (Number(x) || 0); }

  // 把数据池里的数字,填到所有带 data-stat 的元素
  function render() {
    var nodes = root.querySelectorAll('[data-stat]');
    for (var i = 0; i < nodes.length; i++) {
      var key = nodes[i].getAttribute('data-stat');
      if (key in DATA) nodes[i].textContent = fmt(DATA[key]);
    }
  }

  // 计算“当前值 vs 基准值”的涨跌:返回显示文本和颜色类名
  function calcTrend(cur, prev) {
    cur = Number(cur) || 0; prev = Number(prev) || 0;
    if (prev === 0) {                                  // 基准为 0 无法算百分比
      return cur > 0 ? { t: '新增', c: 'up' } : { t: '—', c: 'flat' };
    }
    var p = (cur - prev) / prev * 100;
    if (p === 0) return { t: '持平', c: 'flat' };
    return { t: (p > 0 ? '↑' : '↓') + Math.abs(p).toFixed(0) + '%', c: p > 0 ? 'up' : 'down' };
  }
  // 把所有带 data-trend 的元素填上环比结果并染色
  function renderTrends() {
    var nodes = root.querySelectorAll('[data-trend]');
    for (var i = 0; i < nodes.length; i++) {
      var key = nodes[i].getAttribute('data-trend');
      var base = TREND_MAP[key];
      if (!base) continue;
      var r = calcTrend(DATA[key], DATA[base]);
      nodes[i].textContent = r.t;
      nodes[i].className = 'ust-trend is-' + r.c;
    }
  }

  // 发请求小工具
  function api(path, opt) {
    return fetch(apiBase + path, opt || {}).then(function (r) {
      if (!r.ok) throw new Error('HTTP ' + r.status);
      return r.json();
    });
  }
  // 新版 Umami 要求的两个“只读分享”请求头
  function authHeaders() {
    return { 'x-umami-share-context': '1', 'x-umami-share-token': token };
  }

  // ===== 4. 按北京时间计算各时段的起止时间(毫秒),本周以周一为起点 =====
  function ranges() {
    var now = Date.now();
    var c = new Date(now + H8);               // 用 getUTC* 读到的就是北京的年月日
    var y = c.getUTCFullYear(), mo = c.getUTCMonth(), da = c.getUTCDate();

    var today = Date.UTC(y, mo, da) - H8;              // 今天 00:00
    var yesterday = today - DAY;                        // 昨天 00:00
    var dm = (c.getUTCDay() === 0 ? 6 : c.getUTCDay() - 1);  // 距本周一的天数
    var monday = Date.UTC(y, mo, da - dm) - H8;         // 本周一 00:00
    var lastMonday = monday - 7 * DAY;                  // 上周一 00:00
    var month1 = Date.UTC(y, mo, 1) - H8;               // 本月 1 号 00:00
    var lastMonth1 = Date.UTC(y, mo - 1, 1) - H8;       // 上月 1 号 00:00(mo-1 跨年自动换算)

    return {
      all:           [0, now],                          // 累计
      today:         [today, now],                      // 今天 00:00 到现在
      yesterday:     [yesterday, today],                // 昨天全天(用于“昨日”展示)
      yesterdaySame: [yesterday, yesterday + (now - today)],  // 昨天同一时刻(今日环比基准,保证时间长度相同)
      week:          [monday, now],                     // 本周一到现在
      lastWeekSame:  [lastMonday, lastMonday + (now - monday)], // 上周同一时刻
      month:         [month1, now],                     // 本月 1 号到现在
      lastMonthSame: [lastMonth1, lastMonth1 + (now - month1)] // 上月同一时刻
    };
  }

  // 用分享链接换取 websiteId 和只读 token
  function getToken() {
    return api('/share/' + slug).then(function (d) {
      wid = d.websiteId; token = d.token;
    });
  }
  // 取某个时间段的汇总:访问量 pageviews、访客 visitors、访问次数 visits
  function stats(pair) {
    var url = '/websites/' + wid + '/stats?startAt=' + pair[0] + '&endAt=' + pair[1] + '&unit=year';
    return api(url, { headers: authHeaders() });
  }
  // 把一次接口返回的三个指标,按前缀写进数据池
  function fill(prefix, d) {
    DATA[prefix + 'PV']     = val(d.pageviews);
    DATA[prefix + 'UV']     = val(d.visitors);
    DATA[prefix + 'Visits'] = val(d.visits);
  }

  // 一次性拉取全部时段,填数据池后刷新数字与环比
  function loadStats() {
    var r = ranges();
    return Promise.all([
      stats(r.all), stats(r.today), stats(r.yesterday), stats(r.yesterdaySame),
      stats(r.week), stats(r.lastWeekSame), stats(r.month), stats(r.lastMonthSame)
    ]).then(function (s) {
      fill('all', s[0]);       fill('today', s[1]);
      fill('yesterday', s[2]); fill('prevDay', s[3]);
      fill('week', s[4]);      fill('prevWeek', s[5]);
      fill('month', s[6]);     fill('prevMonth', s[7]);
      render();
      renderTrends();
    });
  }
  // 单独拉取当前在线人数
  function loadActive() {
    return api('/websites/' + wid + '/active', { headers: authHeaders() })
      .then(function (a) { DATA.online = val(a.visitors); render(); });
  }

  // 页面切到后台标签时不刷新,省流量
  function visible() { return document.visibilityState !== 'hidden'; }
  // 定时任务:先立即执行一次,之后按间隔执行;页面不可见时跳过
  function every(fn, ms) {
    function run() {
      fn().catch(function () {                    // 失败时重新换取 token 再试一次
        return getToken().then(fn).catch(function () {});
      });
    }
    run();
    setInterval(function () { if (visible()) run(); }, ms);
  }

  // ===== 5. 启动 =====
  function start() {
    getToken().then(function () {
      every(loadStats, STATS_INTERVAL);
      every(loadActive, ACTIVE_INTERVAL);
      // 从后台切回页面时立刻刷新一次
      document.addEventListener('visibilitychange', function () {
        if (visible()) { loadStats().catch(function(){}); loadActive().catch(function(){}); }
      });
    }).catch(function () {
      setTimeout(start, 15000);                  // 网络失败 15 秒后自动重试
    });
  }
  start();
})();
</script>
<!-- ========== Umami 纯文字实时统计 · 结束 ========== -->

 

 

四、如何部署到你的网站

这段代码不依赖任何框架,凡是能放 HTML 的地方都能用。下面分场景说明,重点看「运行代码」和「展示代码」的区别。

1. 纯静态 HTML / 任意能直接写源码的网站

把整段代码直接粘到页面 <body> 里想显示的位置(比如 footer)即可,无需任何插件。

2. WordPress(经典编辑器 / 古腾堡 / 小工具)

  • 想让挂件在全站前台运行:推荐安装 WPCode 或 Insert Headers and Footers,新建一段代码,把原始运行代码(即上文第三部分的代码,未做任何转义)粘进去并设置在页脚输出;或在「外观 → 小工具 → 自定义 HTML」里粘贴。
  • 只是在文章里写教程给读者看:请在编辑器右上角切到 「代码」 模式(经典编辑器),粘贴的是本文这种用 <pre><code> 包裹的 HTML;不要在「可视化」模式直接贴,否则 <script> 会被过滤掉。

3. Typecho / Halo / 其它 CMS 或博客系统

在主题的「自定义 HTML / 自定义代码 / 页脚统计代码」位置,粘贴原始运行代码;如果是在文章里展示给读者,同样要放到 HTML 源码模式的 <pre><code> 中。

关键区别:让挂件在你自己网站「跑起来」用的是未转义的原始代码(放小工具 / 插件 / 主题);而在文章里「展示给读者看」用的是

 包裹的代码,里面的 < > 已转义成 < >,这样它只会显示成文字、不会被执行。两者不要放错位置。

五、如何自定义(只改 HTML,不用动 JS)

  1. 换网站:只改 <div> 上的 data-share-url,换成该站的分享链接。
  2. 改刷新频率:脚本顶部 ACTIVE_INTERVAL(在线人数)和 STATS_INTERVAL(其余数据),单位是毫秒。
  3. 增删条目:只在 <div> 里增删 <span>,可用的 data-stat / data-trend 值都列在代码注释里。
  4. 改外观:字号改 font-size,分隔符改 content:"·",涨跌颜色改 .is-up / .is-down。
  5. 同页面放多个网站:复制多份后,把 id="umami-stats-text" 改成不同名字(例如 umami-stats-text-2),并同步修改 JS 里的 getElementById,否则会冲突。

六、指标口径说明(避免误解)

  • 访问量(PV):页面被浏览的次数;访客(UV):独立访客数;访问次数(Visits):会话数。
  • 在线人数:Umami 定义为「最近 5 分钟内有活动的访客」,并不是精确瞬时并发。Umami 的分享接口本身不支持 WebSocket 推送,因此「实时」只能靠定时轮询,5 秒已经与 Umami 官方实时页一致。
  • 所有时段固定按北京时间(UTC+8)统计,本周以周一为起点。
  • 环比为什么用「昨日同时段」:因为今天还没结束,直接拿「今日 vs 昨日全天」并不公平。代码用的是「今天 0 点到现在」对比「昨天 0 点到昨天同一时刻」,时间长度相同;本周、本月同理。
  • 基准值为 0 时显示「新增」(绿色),数值相等显示「持平」,其余显示 ↑/↓ + 百分比。

七、常见问题

  • 显示不出来?先确认分享链接已开启、data-share-url 没复制错;再确认代码放在 HTML 源码模式(<script> 没有被可视化编辑器吞掉);浏览器按 F12 看控制台一般会有提示。
  • 会不会拖慢网站?不会。统计是异步请求,页面在后台标签时会自动暂停轮询。访客量较大时,可把 STATS_INTERVAL 调到 30000(30 秒)以减少请求。
  • 安全吗?只读令牌只能看到分享的数据,不含任何账号或写权限,随时可在 Umami 后台关闭该分享链接。
  • Umami Cloud 能用吗?可以,把 data-share-url 换成 Cloud 对应的分享链接即可,代码完全通用。

以上就是整个挂件的部署与自定义方法。它是纯原生 JavaScript,不依赖任何框架和建站程序,任何能放自定义 HTML 的站点都可以直接使用。

版权声明:
作者:蓝逸轩
链接:https://www.12xf.cn/1304.html
来源:星锋网
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
< <上一篇
下一篇>>