- 数据分析
- 后端
【免费下载链接】goatcounter
Easy web analytics. No tracking of personal data.
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:
- 响应头被设置为
Content-Type: image/gif并附带Access-Control-Allow-Origin: *(支持跨域上报); - 先用
isbot.Bot(r)做服务端机器人检测——浏览器端count.js的is_bot()(public/count.js)只能识别无头浏览器等客户端特征,服务端会以isbot库的判定为准覆盖hit.Bot(源码注释明确写了 "Prefer the backend detection"); - 通过
formam解码 URL 查询参数到goatcounter.Hit结构,校验path长度(超过 2048 字节直接拒绝)、bot 值合法性等; - 校验通过后写入
goatcounter.Memstore.Append(hit)——先进入内存缓冲,再异步落入数据库; - 最后返回一个 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点击查看免费下载相关推荐
在 Webpack 中使用 StyleX:基于 @stylexjs/unplugin 的完整接入实战指南
在 Webpack 中使用 StyleX:基于 @stylexjs/unplugin 的完整接入实战指南 StyleX 是面向复杂用户界面的样式系统,其核心思路
前端VitePress 接入 Headless CMS:基于动态路由与数据加载器的完整实践指南
VitePress 接入 Headless CMS:基于动态路由与数据加载器的完整实践指南 导读 本文讲解如何将 VitePress 与各类 Headless
前端文档@scalar/sveltekit 集成指南:在 SvelteKit 中接入 Scalar API Reference 的完整实践
@scalar/sveltekit 集成指南:在 SvelteKit 中接入 Scalar API Reference 的完整实践 本篇技术指南以 integr
开发工具API 工具前端
上一篇:轻松掌握Blender3mfFormat:3MF文件高效处理指南下一篇:Rpisurv智能重排原理:摄像头断线后画面如何自动重排创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考