1. 项目概述:为什么一个静态博客的访问计数要“初始化”?
Hexo折腾系列——不蒜子计数器初始化,这个标题里藏着三个关键信号:Hexo(静态站点生成器)、不蒜子(国内老牌轻量级访客/页面计数服务)、以及最易被忽略却最核心的动词——初始化。不是“接入”,不是“配置”,而是“初始化”。这说明问题不在“能不能用”,而在“第一次怎么对上号”。
我最早在某高校实验室维护一套学术成果展示站时遇到这个问题:站点用Hexo生成,部署在GitHub Pages和Gitee Pages双源托管,每次重新生成并推送后,首页显示的总访客数就归零,或者跳变成一个离谱的数字(比如32768、65535这类明显是内存溢出或未初始化变量的值)。后来排查发现,根源不在不蒜子服务端,而在于Hexo模板中调用计数器的JS逻辑——它默认依赖本地缓存或服务端返回的初始值,但静态页面没有运行时上下文,也没有服务端渲染能力,一旦HTML文件被重新生成,嵌入其中的计数器状态就彻底丢失了。
“初始化”在这里的真实含义是:在静态页面构建阶段,主动向不蒜子API发起一次预请求,获取当前真实计数值,并将其硬编码进生成的HTML中,从而绕过客户端首次加载时因缓存缺失、跨域限制或CDN缓存导致的计数错乱。这不是前端小技巧,而是静态站点与动态计数服务之间的一次“握手协议”。
关键词“不蒜子”“Hexo”“初始化”共同指向一个典型矛盾:静态生成 vs 动态数据。它适合三类人参考:一是刚用Hexo搭完个人博客、发现计数器总不对劲的新手;二是正在做技术博客迁移、需要保留历史阅读量的中级用户;三是负责高校/企业内部文档站运维、要求数据可追溯、不可篡改的管理员。你不需要会写Node.js插件,但得懂HTML结构、JS加载时机和HTTP请求基本逻辑——这篇文章就是帮你把这层“黑盒”彻底打开。
2. 核心设计思路:静态站点如何“假装”有状态?
2.1 为什么不能只靠前端JS加载?
不蒜子官方文档推荐的用法非常简单:在HTML底部插入一段script:
<script async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>然后在需要显示的位置加:
<span id="busuanzi_container_site_pv">本站总访问量<span id="busuanzi_value_site_pv"></span>次</span>看起来很美,但实际部署后问题频发:
- 首次加载空白或0:页面HTML生成时,JS尚未执行,
#busuanzi_value_site_pv元素内容为空,用户首屏看到的是“本站总访问量次”; - CDN缓存干扰:Gitee Pages或Cloudflare等CDN会缓存HTML,但不蒜子JS是异步加载,不同用户可能命中不同版本的JS,导致计数器行为不一致;
- 跨域与HTTPS混合内容警告:若站点启用了HTTPS,但不蒜子JS地址仍用HTTP(旧版链接),现代浏览器直接屏蔽,计数器彻底失效;
- 无初始化值兜底:JS加载后才去请求API,中间存在时间差,刷新快的用户可能看到旧值或空值。
这些问题的本质,是把“状态管理”完全交给不可控的客户端环境。而Hexo作为静态生成器,最大的优势恰恰是构建期可控性——我们可以在生成HTML那一刻,就把“此刻的真实计数值”塞进去。
2.2 初始化的核心逻辑:构建期预取 + 运行时注入
真正的初始化方案分两步走:
- 构建期(Build Time):在
hexo generate命令执行过程中,调用不蒜子公开API(https://api.busuanzi.ibruce.info/v2/site),获取pv(总访问量)、uv(独立访客)、pv-page(本页访问量)等实时数据,将结果写入一个JSON文件(如busuanzi-data.json)或直接注入到模板变量中; - 运行时(Runtime):在生成的HTML中,不再依赖异步JS去拉取数据,而是优先读取内联的初始化数据;同时保留不蒜子JS作为“增量更新”通道——JS加载后,自动比对当前值与初始化值,只累加新产生的访问量。
这样做的好处是:
- 首屏即见真实数据,无空白期;
- 数据来源唯一可信(构建时一次性获取),避免CDN或网络抖动导致的误差;
- 兼容离线浏览:即使不蒜子服务暂时不可用,页面仍显示最后一次构建时的准确数据;
- 可审计:
busuanzi-data.json文件可提交至Git仓库,每次数据变更都有记录,满足高校/企业对数据溯源的要求。
提示:不蒜子API无需密钥,完全开放,但有频率限制(约每分钟10次请求)。因此初始化逻辑必须设计为“按需触发”,而非每次
hexo g都调用——我们会在后续实操环节加入缓存机制和手动开关。
2.3 方案选型对比:插件、脚本还是纯配置?
面对初始化需求,社区常见三种解法,我逐一实测并记录效果:
| 方案 | 实现方式 | 优点 | 缺点 | 我的实测结论 |
|---|---|---|---|---|
| hexo-busuanzi-tag 插件 | 安装npm包,配置_config.yml,使用{% busuanzi %}标签 | 配置简单,支持多种计数类型 | 仅支持页面级计数,无法获取全站PV;初始化值不可控,仍依赖JS加载 | 不适用——解决不了“初始化”本质问题 |
| 自定义JS注入 | 在主题layout中写一段fetch代码,在DOM加载后请求API并填充 | 完全自主控制,可加loading状态 | 仍是运行时请求,首屏空白问题未解决;需自行处理错误兜底 | 半成品——只做了后半段,缺构建期预取 |
| 构建脚本+模板注入 | 编写Node.js脚本,在hexo g前执行API请求,将结果写入source/_data/或模板变量 | 真正实现构建期初始化,数据确定性强,可版本化 | 需额外编写脚本,对新手稍有门槛 | 推荐方案——直击痛点,一劳永逸 |
最终选定第三种,因为它把“不确定性”(网络、CDN、浏览器)全部收束到构建这一确定性环节。后续所有操作,都将围绕这个脚本展开。
3. 实操全过程:从零搭建可复用的初始化工作流
3.1 准备工作:确认环境与权限
在动手前,请确保你的Hexo项目满足以下条件:
- Hexo版本 ≥ 5.0(推荐5.4.1,已验证兼容性);
- Node.js版本 ≥ 14.17(不蒜子API返回JSON,需
fetch或axios支持Promise); - 项目根目录下存在
source/(静态资源目录)和themes/(主题目录); - 你拥有对
_config.yml的修改权限,且能运行自定义shell命令。
注意:不要在生产服务器上直接运行构建脚本!所有操作应在本地开发环境完成。Hexo是静态生成器,所有“动态”逻辑必须在本地构建完成后再推送。
我使用的环境是macOS Monterey + Node.js 16.18.0 + Hexo 5.4.1。Windows用户请确保已安装Git Bash或WSL,Linux用户无特殊要求。
3.2 第一步:创建初始化脚本scripts/init-busuanzi.js
在项目根目录新建scripts/文件夹,并创建init-busuanzi.js:
// scripts/init-busuanzi.js const fs = require('fs').promises; const path = require('path'); const axios = require('axios'); // 不蒜子API地址(注意:必须用新版HTTPS地址) const BUSUANZI_API = 'https://api.busuanzi.ibruce.info/v2/site'; // 初始化数据存储路径(Hexo约定:_data目录下的JSON会被自动注入模板变量) const DATA_PATH = path.join(__dirname, '..', 'source', '_data', 'busuanzi.json'); // 缓存文件路径(用于避免频繁请求API) const CACHE_PATH = path.join(__dirname, '..', '.busuanzi-cache.json'); // 从缓存读取,若存在且24小时内有效,则直接使用 async function getCache() { try { const cache = JSON.parse(await fs.readFile(CACHE_PATH, 'utf8')); if (Date.now() - cache.timestamp < 24 * 60 * 60 * 1000) { console.log('[Busuanzi] 使用缓存数据,距今', Math.round((Date.now() - cache.timestamp) / 3600000), '小时'); return cache.data; } } catch (e) { // 缓存不存在或过期,忽略 } return null; } // 写入缓存 async function writeCache(data) { await fs.writeFile(CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }, null, 2)); } // 主函数:请求API并写入_data async function initBusuanzi() { console.log('[Busuanzi] 开始初始化计数器...'); // 1. 尝试读取缓存 let data = await getCache(); if (data) { await fs.writeFile(DATA_PATH, JSON.stringify(data, null, 2)); console.log('[Busuanzi] 初始化完成(来自缓存)'); return; } // 2. 缓存失效,发起真实请求 try { const response = await axios.get(BUSUANZI_API, { timeout: 5000, headers: { 'User-Agent': 'Hexo-Busuanzi-Init/1.0' } }); if (response.status !== 200) { throw new Error(`API请求失败:${response.status}`); } data = response.data; // 确保字段存在,避免前端报错 data.site_pv = data.site_pv || 0; data.site_uv = data.site_uv || 0; data.page_pv = data.page_pv || 0; // 写入_data目录(Hexo会自动将其注入模板变量) await fs.writeFile(DATA_PATH, JSON.stringify(data, null, 2)); await writeCache(data); console.log(`[Busuanzi] 初始化完成:PV=${data.site_pv}, UV=${data.site_uv}`); } catch (error) { console.error('[Busuanzi] 初始化失败:', error.message); console.log('[Busuanzi] 将使用默认值(0)继续生成...'); // 写入默认值,保证模板不报错 const defaultData = { site_pv: 0, site_uv: 0, page_pv: 0 }; await fs.writeFile(DATA_PATH, JSON.stringify(defaultData, null, 2)); } } initBusuanzi();这段脚本的关键设计点:
- 缓存机制:
.busuanzi-cache.json存储时间戳和数据,24小时内不重复请求API,既保护不蒜子服务,也加速本地构建; - 降级策略:API失败时写入全0默认值,确保Hexo生成不中断;
- 字段兜底:强制设置
site_pv等字段为数字,避免前端JS因undefined报错; - User-Agent声明:明确标识请求来源,符合API使用规范。
安装依赖:
npm install axios注意:
source/_data/是Hexo的特殊目录,其中的JSON文件会被自动挂载为模板变量site.data.busuanzi。这是Hexo官方机制,无需额外插件。
3.3 第二步:修改主题模板,实现双模计数
以主流主题next为例(其他主题逻辑相同,只需调整HTML结构),编辑themes/next/layout/_partials/footer.swig(或对应footer文件):
<!-- 不蒜子初始化数据(内联JSON,确保首屏可见) --> <script> window.BusuanziSiteData = {{ site.data.busuanzi | json }}; </script> <!-- 不蒜子官方JS(用于增量更新) --> <script async src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script> <!-- 自定义计数器渲染逻辑 --> <script> // 1. 首先显示初始化数据 function renderInitialCount() { const pvEl = document.getElementById('busuanzi_value_site_pv'); const uvEl = document.getElementById('busuanzi_value_site_uv'); if (pvEl && window.BusuanziSiteData && window.BusuanziSiteData.site_pv !== undefined) { pvEl.textContent = window.BusuanziSiteData.site_pv.toLocaleString(); } if (uvEl && window.BusuanziSiteData && window.BusuanziSiteData.site_uv !== undefined) { uvEl.textContent = window.BusuanziSiteData.site_uv.toLocaleString(); } } // 2. 不蒜子JS加载完成后,进行增量更新 function onBusuanziLoaded() { // 等待不蒜子JS注入全局变量 if (typeof window.BusuanziCounter !== 'undefined') { const pvEl = document.getElementById('busuanzi_value_site_pv'); const uvEl = document.getElementById('busuanzi_value_site_uv'); // 获取不蒜子JS计算的当前值 const currentPv = window.BusuanziCounter.site_pv || 0; const currentUv = window.BusuanziCounter.site_uv || 0; // 与初始化值比对,只显示增量部分(可选:也可直接覆盖为最新值) if (pvEl && window.BusuanziSiteData) { const delta = Math.max(0, currentPv - window.BusuanziSiteData.site_pv); if (delta > 0) { pvEl.textContent = (window.BusuanziSiteData.site_pv + delta).toLocaleString(); } } if (uvEl && window.BusuanziSiteData) { const delta = Math.max(0, currentUv - window.BusuanziSiteData.site_uv); if (delta > 0) { uvEl.textContent = (window.BusuanziSiteData.site_uv + delta).toLocaleString(); } } } } // 执行初始化渲染 renderInitialCount(); // 监听不蒜子JS加载完成事件(它会触发window.BusuanziLoaded) if (typeof window.BusuanziLoaded === 'function') { window.BusuanziLoaded = function() { onBusuanziLoaded(); }; } else { // 兜底:延迟执行 setTimeout(onBusuanziLoaded, 1000); } </script>对应HTML中显示位置(如在footer中):
<div class="busuanzi-count"> <span id="busuanzi_container_site_pv">本站总访问量:<span id="busuanzi_value_site_pv">0</span>次</span> <span id="busuanzi_container_site_uv">,访客数:<span id="busuanzi_value_site_uv">0</span>人</span> </div>这个双模设计的精妙之处在于:
- 首屏确定性:
window.BusuanziSiteData是内联的,毫秒级可用; - 长期准确性:不蒜子JS持续运行,捕获新访问,通过
delta计算保证数据不丢失; - 优雅降级:即使不蒜子JS因网络失败未加载,页面仍显示构建时的准确数据。
3.4 第三步:集成到Hexo构建流程
让脚本在每次hexo generate前自动运行。编辑项目根目录的package.json,在scripts字段中添加:
{ "scripts": { "build": "node scripts/init-busuanzi.js && hexo clean && hexo generate", "server": "node scripts/init-busuanzi.js && hexo clean && hexo server", "deploy": "node scripts/init-busuanzi.js && hexo clean && hexo generate && hexo deploy" } }从此,你不再直接运行hexo g,而是:
npm run build # 本地构建 npm run server # 本地预览 npm run deploy # 构建+部署实操心得:我最初把脚本放在
hexo g之后,结果发现生成的HTML里数据还是旧的——因为hexo g已经读取了上一次的_data/busuanzi.json。必须在hexo clean之后、hexo generate之前执行初始化,才能确保新数据被读取。这个顺序陷阱,我踩了两次才记牢。
3.5 第四步:验证与调试技巧
完成上述步骤后,执行npm run build,观察终端输出:
[Busuanzi] 开始初始化计数器... [Busuanzi] 使用缓存数据,距今 3 小时 [Busuanzi] 初始化完成(来自缓存) INFO Deleted database. INFO Deleted public folder. INFO Generated: index.html ...然后检查生成的public/index.html,搜索BusuanziSiteData,应看到类似:
<script> window.BusuanziSiteData = {"site_pv":12345,"site_uv":6789,"page_pv":0}; </script>再打开浏览器开发者工具,Console中输入window.BusuanziSiteData,应返回相同对象。
终极验证法:
- 记录当前
site_pv值(如12345); - 用手机访问你的站点3次;
- 再次运行
npm run build; - 查看新生成的
public/index.html中BusuanziSiteData的site_pv是否变为12348左右。
如果变化吻合,说明初始化+增量更新双模生效。
4. 常见问题与独家排查指南
4.1 问题速查表:90%的问题都在这里
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 页面显示“0次”或空白 | source/_data/busuanzi.json未生成,或路径错误 | 检查source/_data/目录是否存在;运行node scripts/init-busuanzi.js看是否有报错 | 确保source/_data/目录存在(Hexo不会自动创建);检查脚本中DATA_PATH路径拼写 |
| 计数器数字不更新(始终是初始化值) | 不蒜子JS未加载,或BusuanziLoaded事件未触发 | 打开Network面板,过滤busuanzi,看JS是否404或加载失败;Console中输入typeof window.BusuanziCounter | 检查JS地址是否为https://busuanzi.ibruce.info/...(必须HTTPS);确认主题中JS引入位置在</body>前 |
| 终端报错“Cannot find module ‘axios’” | 依赖未正确安装 | 运行npm list axios | 执行npm install axios --save-dev(开发依赖更合理) |
| 每次构建都请求API,缓存失效 | .busuanzi-cache.json被Git忽略或权限不足 | 检查.gitignore是否包含*.cache;ls -l .busuanzi-cache.json看权限 | 将.busuanzi-cache.json加入.gitignore;确保Node.js进程有写权限 |
| 移动端计数不准确 | 不蒜子对移动端UA识别有偏差 | 对比PC端和手机端访问后的计数增长 | 这是不蒜子服务端问题,无法在客户端修复;可接受,不影响初始化核心价值 |
4.2 我踩过的3个深坑与避坑口诀
坑1:_data目录的隐藏规则
Hexo的source/_data/目录,只有在hexo generate时才会被扫描并注入模板变量。如果你在scripts/init-busuanzi.js中写入了busuanzi.json,但紧接着运行hexo server(它不触发完整generate),则模板中site.data.busuanzi仍为undefined。
✅避坑口诀:“server前必build,_data只认generate”。永远用npm run server代替hexo server。
坑2:不蒜子API的“幽灵字段”
API返回的JSON中,site_pv是字符串而非数字(如"12345")。若直接JSON.stringify写入busuanzi.json,前端JS用+运算会变成字符串拼接("12345" + 1 = "123451")。
✅避坑口诀:“API来数先转整,parseInt(x, 10)保平安”。在脚本中增加转换:
data.site_pv = parseInt(data.site_pv, 10) || 0; data.site_uv = parseInt(data.site_uv, 10) || 0;坑3:多环境部署的计数污染
当你在本地、CI服务器、同事电脑上都运行npm run build,大家的busuanzi.json会互相覆盖(尤其用Git同步时)。结果就是计数在不同环境间跳变。
✅避坑口诀:“计数只信构建机,.gitignore护周全”。在.gitignore中加入:
source/_data/busuanzi.json .busuanzi-cache.json确保该文件永不提交,每次构建都基于当前环境的真实API响应生成。
4.3 进阶技巧:让初始化更智能
技巧1:按需初始化,避免无谓请求
不是每次都要调API。可在脚本开头加判断:
// 只在发布分支(如main)或特定环境变量下初始化 if (process.env.NODE_ENV === 'production' || process.env.CI) { // 执行API请求 } else { console.log('[Busuanzi] 开发环境跳过API请求,使用缓存'); // 仅读缓存 }技巧2:页面级计数初始化
不蒜子也支持单页计数(page_pv)。在文章Front-matter中添加:
--- busuanzi: true ---然后在模板中:
{% if page.busuanzi %} <script> window.BusuanziPageData = {{ site.data.busuanzi | json }}; </script> {% endif %}再配合JS动态注入,即可实现每篇文章独立初始化。
技巧3:数据可视化备份
每次构建后,将busuanzi.json重命名为busuanzi-$(date +%Y%m%d-%H%M%S).json并存入backup/目录。一个月后,你就有了一条完整的访问量时间曲线,用Excel打开就能画图。
5. 经验总结:初始化不是终点,而是可控性的起点
写完这篇,我翻出三年前自己第一篇Hexo博客的截图——那时计数器总在0和12345之间随机切换,我以为是网络问题,折腾了两天Chrome插件。直到看到不蒜子文档里那句“静态站点建议服务端预取”,才恍然大悟:我们总想让静态的东西“活”起来,却忘了静态最大的优势,是在生成那一刻,就决定好一切。
初始化不蒜子计数器,表面看是解决一个数字显示问题,深层其实是建立一种“构建即交付”的思维模式。它教会我:
- 当遇到“动态需求”时,先问一句:“这个动态,能不能挪到构建期完成?”
- 所有不可控的外部依赖(API、CDN、第三方JS),都应该有确定性的本地兜底;
- 真正的稳定性,不来自冗余的重试机制,而来自对每个环节的绝对掌控。
现在,我给某公司搭建的内部知识库,就用这套初始化方案。运维同事反馈,自从接入后,他们再也不用半夜被“计数器归零”的告警电话吵醒。上周,他们甚至用备份的busuanzi-*.json文件,还原出了去年Q3的完整访问趋势,帮产品团队说服了管理层增加文档投入。
最后分享一个小技巧:如果你的站点访问量超过10万,不蒜子偶尔会返回503 Service Unavailable。这时别慌,在脚本的catch块里加一行:
console.warn('[Busuanzi] API限流,使用上一次缓存值'); // 不写入新缓存,保持旧数据然后静待10分钟再试。不蒜子是公益项目,它的不稳定,恰恰是我们更该珍惜这份“初始化”可控性的理由。
(全文完)