☰
GoatCounter 在 SPA 应用中的手动计数接入:基于 hashchange 与 count() API 的完整实践
2026/10/10 1:49:29 网站建设 项目流程
  • 数据分析
  • 后端

【免费下载链接】goatcounter

Easy web analytics. No tracking of personal data.

项目地址:https://gitcode.com/gh_mirrors/go/goatcounter
点击查看免费下载

GoatCounter 是一款轻量、不追踪个人数据的网站分析工具。默认的count.js会在页面加载时自动上报一次访问,但这对于以#片段(hash)驱动路由的单页应用(SPA)并不适用——hash 变化不会触发页面重载。本文以仓库 tpl/help/spa.md 为核心,讲解如何通过设置no_onload关闭自动统计,并监听hashchange事件手动调用window.goatcounter.count(),实现 SPA 中每一次路由切换的精确访问统计。读完本文,你将掌握 GoatCounter 手动计数的完整配置方法、count()与path参数的底层行为,以及事件跟踪、会话判定等配套能力。

原文档示例:为#导航的 SPA 接入手动统计

spa.md给出的方案非常简洁,全部代码如下(稍作排版整理):

<script> window.goatcounter = {no_onload: true} window.addEventListener('hashchange', function(e) { window.goatcounter.count({ path: location.pathname + location.search + location.hash, }) }) </script> <script>if (!goatcounter.no_onload) on_load(function() { // 1. Page is visible, count request. // 2. Page is not yet visible; wait until it switches to 'visible' and count. if (!('visibilityState' in document) || document.visibilityState === 'visible') goatcounter.count() else { var f = function(e) { if (document.visibilityState !== 'visible') return document.removeEventListener('visibilitychange', f) goatcounter.count() } document.addEventListener('visibilitychange', f) } if (!goatcounter.no_events) goatcounter.bind_events() })

从源码结构可以清晰看到no_onload的双重作用:

  • 不再自动调用count():默认行为是页面可见即上报一次;若页面处于后台加载(visibilityState不是visible),还会等待切换为可见后再上报。设置no_onload后这一切都被跳过;
  • 同时不再绑定事件:源码中bind_events()(为带data-goatcounter-click属性的元素绑定点击统计)也在同一个on_load回调里,因此no_onload: true时它同样不会执行。如果你需要两者之一,可以只设置no_events来单独禁用事件绑定,详见 tpl/help/js.md 中的设置说明表。

设置项也可以通过<script>标签的data-goatcounter-settings属性传入(必须是合法 JSON):

<script>变量说明默认值path页面路径(不含域名)或事件名<link rel="canonical">或location.pathname + location.searchtitle人类可读的标题document.titlereferrer来源,可以是 URL 或任意字符串使用Referer头event把path当作事件而非 URL,布尔值falseno_session不跟踪会话,每次上报都计入由站点设置决定

这些参数同样可以通过data-goatcounter-settings或window.goatcounter全局设置,count()调用时传入的vars会合并覆盖全局值。

hashchange 之外:适配各种 SPA 路由方案

原文档只覆盖了#hash 路由,但其背后的思路可以推广到所有"路由变化但不触发整页加载"的 SPA 架构:

1. History API 路由(pushState/replaceState):使用history.pushState()的 SPA 不会触发hashchange,需要监听popstate(浏览器前进/后退)并自行包装路由调用:

window.goatcounter = {no_onload: true} // 首次进入也计一次。 window.goatcounter.count({path: location.pathname + location.search}) // 在路由库的 afterEach 钩子里调用,或在 pushState 后手动触发。 window.addEventListener('popstate', function() { window.goatcounter.count({path: location.pathname + location.search}) })

注意:直接监听popstate只能覆盖前进/后退,pushState本身不触发任何事件,因此实践中通常要在路由框架的钩子(如 Vue Router 的afterEach、React Router 的useEffect或监听 location)中统一调用count()。

2. 事件跟踪:SPA 中用户的交互(按钮点击、表单提交、下载等)可以借助event: true上报为事件而非页面访问,见 tpl/help/events.md:

window.goatcounter.count({ path: 'click-banana', title: 'Yellow curvy fruit', event: true, })

事件名的path不能以/开头;如果想记录事件发生所在的路径,可以用回调把默认 path 拼进事件名:

window.goatcounter.count({ path: function(p) { return 'click-banana-' + p }, event: true, })

3. 结合no_session:GoatCounter 默认按"访次"(visit)而非"浏览量"(pageview)统计,同一用户在 8 小时内重复刷新同一路径只算一次(技术细节见 tpl/help/sessions.md)。若某个事件希望每次触发都计一次(例如按钮连点),可以加no_session: true。

源码级验证:从浏览器到数据库的完整链路

手动调用count()后发生了什么?服务端对应的是 handlers/count.go 中的counthandler:

  1. 响应头被设置为Content-Type: image/gif并附带Access-Control-Allow-Origin: *(支持跨域上报);
  2. 先用isbot.Bot(r)做服务端机器人检测——浏览器端count.js的is_bot()(public/count.js)只能识别无头浏览器等客户端特征,服务端会以isbot库的判定为准覆盖hit.Bot(源码注释明确写了 "Prefer the backend detection");
  3. 通过formam解码 URL 查询参数到goatcounter.Hit结构,校验path长度(超过 2048 字节直接拒绝)、bot 值合法性等;
  4. 校验通过后写入goatcounter.Memstore.Append(hit)——先进入内存缓冲,再异步落入数据库;
  5. 最后返回一个 43 字节的 1×1 GIF 像素(源码注释:GIF 是体积最小的选择,PNG 需要 116 字节)。

这条链路意味着:即使count.js上报失败(如 CSP 拦截),用<img>回退或直接构造请求打到/count端点,统计依然成立。这也解释了 tpl/help/countjs-host.md 中"/count端点保证长期兼容,未来不兼容变更会换新端点(如/count/v2)"的承诺。

实用注意事项

1. 脚本加载位置与 async:count.js默认以async加载,你的内联脚本若在它之前执行,window.goatcounter.count可能尚未定义。spa.md示例中把hashchange监听注册在count.js之前是安全的(事件回调在 hash 真正变化时才执行),但如果要在页面加载时立即调用count(),tpl/help/js.md 建议要么去掉async,要么用setInterval轮询等待:

var t = setInterval(function() { if (!window.goatcounter || !window.goatcounter.count) return clearInterval(t) // Do stuff with goatcounter here. }, 100)

2. 过滤逻辑会照常生效:手动count()同样会经过filter()——预渲染页面、iframe(除非allow_frame)、localhost/内网地址(除非allow_local,方便本地联调)都会被自动过滤,并在控制台打印原因。详见 tpl/help/js.md 的filter()方法说明与 public/count.js 的实现。

3. 关于 canonical 链接:get_path()在存在同域(允许www子域)<link rel="canonical">时会优先使用它。SPA 若用查询参数驱动导航,通常不要加 canonical(会抹掉 URL 差异),详见 tpl/help/path.md。

4. 本地测试:设置allow_local: true(通过data-goatcounter-settings或window.goatcounter)即可在localhost上验证整套手动计数流程是否按预期上报。

总结

GoatCounter 对 SPA 的支持并不需要专用 SDK——tpl/help/spa.md中十几行代码的组合(no_onload+hashchange+count({path}))就足以覆盖 hash 路由的统计需求,且可自然推广到 History API 路由与事件跟踪场景。这套方案的全部行为都能在 public/count.js 与 handlers/count.go 的源码中得到验证,数据参数、过滤规则、会话判定等配套能力可分别在 tpl/help/js.md、tpl/help/events.md、tpl/help/sessions.md 中查阅到完整说明。

  • 数据分析
  • 后端

【免费下载链接】goatcounter

Easy web analytics. No tracking of personal data.

项目地址:https://gitcode.com/gh_mirrors/go/goatcounter
点击查看免费下载
上一篇:轻松掌握Blender3mfFormat:3MF文件高效处理指南
下一篇:Rpisurv智能重排原理:摄像头断线后画面如何自动重排

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询