用 Umami 给网站做一个前台实时统计挂件:在线人数、今日 / 昨日 / 本周 / 本月、环比涨跌等(免 API Key)
前言
Umami 是一款隐私友好的开源网站统计工具,但它的后台默认是给站长自己看的,访客在前台看不到数据。本文分享一个不依赖任何框架、不依赖特定建站程序的纯文字统计挂件:只要是能放 HTML 的网站(静态页、WordPress、Typecho、Halo、各种博客系统都行),一段代码就能在前台显示实时流量,还能自动计算「较昨日 / 上周 / 上月」的环比涨跌。
一、最终效果
在页脚、侧边栏或任意页面放一行文字,自动显示:
主要特点:
- 纯文字、无边框无卡片:字号、颜色、字体自动继承你当前主题,放进任何页面都不违和;
- 实时自动刷新:在线人数每 5 秒、其余数据每 10 秒更新,频率可自行调整;
- 安全只读:使用 Umami 的「分享链接」取数,不需要在前端放 API Key,随时可在后台关闭;
- 自由增删:想显示哪几条、加哪些指标,只改 HTML 里一行文字即可,不用碰 JS;
- 自带环比涨跌:今日对比昨日同时段、本周对比上周、本月对比上月,涨绿跌红。
二、准备工作(约 1 分钟)
- 确认你的网站已经接入 Umami 统计(页面里带有
data-website-id的统计脚本)。 - 登录 Umami 后台,进入该网站的 设置 / Websites,找到 分享(Share),点 添加,生成一条分享链接。
- 复制这条分享链接,形如
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)
- 换网站:只改
<div>上的data-share-url,换成该站的分享链接。 - 改刷新频率:脚本顶部
ACTIVE_INTERVAL(在线人数)和STATS_INTERVAL(其余数据),单位是毫秒。 - 增删条目:只在
<div>里增删<span>,可用的data-stat/data-trend值都列在代码注释里。 - 改外观:字号改
font-size,分隔符改content:"·",涨跌颜色改.is-up / .is-down。 - 同页面放多个网站:复制多份后,把
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 的站点都可以直接使用。

