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底部插入一段scriptscript async srchttps://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js/script然后在需要显示的位置加span idbusuanzi_container_site_pv本站总访问量span idbusuanzi_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命令执行过程中调用不蒜子公开APIhttps://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或WSLLinux用户无特殊要求。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 srchttps://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 classbusuanzi-count span idbusuanzi_container_site_pv本站总访问量span idbusuanzi_value_site_pv0/span次/span span idbusuanzi_container_site_uv访客数span idbusuanzi_value_site_uv0/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是否包含*.cachels -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分钟再试。不蒜子是公益项目它的不稳定恰恰是我们更该珍惜这份“初始化”可控性的理由。全文完