ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Hexo博客不蒜子计数器初始化实战指南

Hexo博客不蒜子计数器初始化实战指南

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 初始化的核心逻辑:构建期预取 + 运行时注入

真正的初始化方案分两步走:

  1. 构建期(Build Time):在hexo generate命令执行过程中,调用不蒜子公开API(https://api.busuanzi.ibruce.info/v2/site),获取pv(总访问量)、uv(独立访客)、pv-page(本页访问量)等实时数据,将结果写入一个JSON文件(如busuanzi-data.json)或直接注入到模板变量中;
  2. 运行时(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,应返回相同对象。

终极验证法:

  1. 记录当前site_pv值(如12345);
  2. 用手机访问你的站点3次;
  3. 再次运行npm run build;
  4. 查看新生成的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分钟再试。不蒜子是公益项目,它的不稳定,恰恰是我们更该珍惜这份“初始化”可控性的理由。

(全文完)

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进