前言
统计页的难点不是调用 echarts.init(),而是把 Hexo 的文章模型整理成图表需要的数据,并确保图表只在统计页加载、能适应深色模式、窗口变化和 PJAX 页面切换。
一、统计页由哪几层组成
Hexo 文章、标签、分类 |
项目中对应四个部分:
| 文件 | 作用 |
|---|---|
source/charts/index.md |
提供图表容器和统计卡片 |
scripts/blog-charts.js |
构建时收集并序列化数据 |
source/js/blog-charts-runtime.js |
浏览器端创建 ECharts 实例 |
source/css/blog-charts.css |
卡片尺寸、网格和移动端布局 |
二、为什么选择构建时统计
Hexo 本身已经知道所有文章、标签和分类。如果浏览器打开页面后再请求接口,不但需要额外服务器,也会重复读取数据。
构建阶段直接获取本地模型:
const posts = hexo.locals.get('posts').sort('date', 1).toArray() |
统计结果会随每次 hexo generate 自动更新,线上仍然是纯静态页面。
三、月份折线图的数据怎样准备
如果只统计实际出现过的月份,没有发文的月份会消失,折线图横轴就不连续。例如一月发文、三月发文,不能把二月直接跳过。
脚本先从第一篇文章月份循环到当前月份,每个月填 0:
const firstMonth = moment(posts[0].date).startOf('month') |
再遍历文章累加:
posts.forEach(post => { |
每个月还会附带归档地址:
{ |
所以点击折线上的有效数据点,可以直接进入当月归档。
四、标签 Top 10 怎样产生
标签数据先转换成 ECharts 常见的 { name, value } 结构:
const tagData = tags |
浏览器端读取图表容器的 data-length="10",再截取前十个:
const limit = Number(element.dataset.length) || 10 |
将数量统计和显示数量分开后,将来想改成 Top 15,只改页面属性即可。
五、分类为什么要先构建树
Hexo 分类可以有父子层级。一篇文章可能同时带有父分类和子分类。如果直接把所有分类都算一次,文章会被重复统计。
脚本先把分类转换为节点:
{ |
随后根据 parentId 连接父子关系,并判断文章所带分类中哪些是最深层分类:
const deepestIds = assignedIds.filter(id => |
通俗地说:如果文章同时属于“建站”和“建站/Hexo”,只把它放进更具体的“Hexo”节点。父分类数量再由子节点向上汇总。
即使当前博客分类层级较浅,这种处理也避免以后增加子分类时统计失真。
六、数据怎样安全交给浏览器
统计对象通过脚本写入页面:
window.__BLOG_CHARTS_DATA__ = { |
不能直接把未经处理的 JSON 拼进 <script>。如果标题中包含 </script> 或特殊换行符,可能提前结束脚本。序列化函数会转义危险字符:
const serializeForScript = value => JSON.stringify(value) |
这是把服务端数据嵌入 HTML 时很容易忽略的一步。
七、为什么把 ECharts 打包到本地
项目安装了:
"echarts": "^6.1.0" |
Hexo 生成器把依赖中的压缩文件输出到站点:
hexo.extend.generator.register('blog-charts-vendor', () => ({ |
这样版本由 package-lock.json 固定,不受公共 CDN 突然失效或升级影响。
同时,脚本只有确认页面包含三个图表容器时才注入 ECharts:
if (!chartIds.every(id => html.includes(`id="${id}"`))) { |
首页和普通文章不会下载体积较大的图表库。
八、折线图 option 怎样读
ECharts 的核心是 option。可以把它理解成“图表说明书”:
chart.setOption({ |
xAxis.data决定横轴月份;minInterval: 1防止文章数量出现 0.5 这样的刻度;smooth控制平滑曲线程度;series.data是真正绘制的数据;notMerge: true表示深色模式重绘时使用完整新配置,不残留旧颜色。
面积渐变通过 echarts.graphic.LinearGradient 创建,平均值虚线由 markLine 自动计算。
九、柱状图怎样实现 Top 10
柱状图使用:
series: [{ |
标签较多时横轴文字旋转 24 度,柱顶显示文章数量。点击柱形后读取自定义的 path:
chart.off('click') |
先 off() 再 on() 是为了防止主题切换重绘后重复注册点击事件。
十、分类玫瑰图不是普通饼图
分类图的基础类型仍是饼图:
series: [{ |
普通饼图主要通过扇区角度表现大小;roseType: 'radius' 还会让数量更多的分类拥有更长半径,视觉差异更明显。
提示框中的占比来自 ECharts 自动计算的 params.percent:
formatter: params => |
移动端会缩小半径、字号和引导线,防止标签互相覆盖。
十一、深色模式怎样同步
颜色不是写死一套,而是通过函数读取当前主题:
const isDark = () => |
主题变化时重新执行三张图的 setOption():
new MutationObserver(mutations => { |
同时接入 Butterfly 的 themeChange 全局事件,兼容主题自身的切换流程。
十二、窗口变化为什么需要 resize
ECharts 初始化时会记住容器尺寸。浏览器窗口变化后,仅靠 CSS 改宽度不够,还要调用:
chart.resize() |
为了避免拖动窗口时连续执行几十次,使用 requestAnimationFrame 合并同一帧内的请求:
window.addEventListener('resize', () => { |
十三、构建检查覆盖了什么
验证脚本会确认:
/charts/已生成;- 页面包含统计数据和三个脚本;
- ECharts 没有被加载到首页;
- 分类图仍是
pie + roseType: radius; - 日期范围格式正确;
- 统计卡片链接到归档、分类和标签页。
这样调整图表代码时,不容易无意中破坏关键功能。
十四、维护入口
- 修改卡片和图表容器:
source/charts/index.md - 修改数据统计口径:
scripts/blog-charts.js - 修改折线、柱形和玫瑰图:
source/js/blog-charts-runtime.js - 修改尺寸与移动端布局:
source/css/blog-charts.css
结语
这套统计页的完整链路是“Hexo 构建数据,页面携带数据,ECharts 负责可视化”。理解这三层后,继续增加年度热力图或分类趋势图也会容易很多。






