HEXO 开发笔记(4)自建主题:架构与设计哲学
创建于 2026-06-19
更新于 2026-06-19
科技
hexo
主题开发
架构设计
6726 字 · 约 23 分钟

前言

前几篇笔记介绍了 Hexo 的基础知识、插件体系和进阶功能。从本篇开始,进入 DoraTiger 自建主题的实战开发。本篇作为系列第四篇,聚焦主题的整体架构设计:从 Fan 主题贡献者到独立开发者的转变过程,技术选型背后的思考,以及核心的三层配置合并机制和脚本注入架构。

一、从贡献者到独立开发者

1.1 与 Fan 主题的渊源

我最初使用的是 Fan 主题,并且在 GitHub 上为其贡献过代码,算是 Fan 主题的贡献者之一。使用了相当长一段时间,整体体验不错。

但后来作者长期没有更新,而 Hexo 版本在不断迭代。随着 Hexo 7.x 的发布,一些 API 和行为发生了变化,Fan 主题逐渐出现了兼容性问题。加上我当时正好有空,想深入学习一下 PugStylusES6 Modules 等前端技术,所以决定从头构建一个自己的主题。

1.2 为什么选择一体化集成

最终没有选择继续维护 Fan 的 fork,而是完全重写,主要考虑的是一体化集成的设计理念。Hexo 主题生态中有大量优秀的第三方插件(加密、搜索、sitemap、统计等),但在实际使用中,将这些功能分散在不同插件中会带来一些问题:

  • 各插件的配置格式不统一,维护成本高
  • 插件之间可能存在兼容性冲突
  • 功能逻辑分散在多处,排查问题困难
  • 升级 Hexo 版本时,需要逐个检查插件兼容性

DoraTiger 主题选择将常用功能一体化集成到主题内部,方便统一处理逻辑,减少外部依赖带来的维护成本。所有集成的功能均在主题 README.md 中标注了原始来源和引用链接,确保开源精神。

1.3 重写目标

  • 一体化集成 — 加密、搜索、sitemap、统计、外链拦截等功能直接内置
  • 暗色主题 + Canvas 动画
  • 完整的配置文档(docs/CONFIG.md,1051 行)
  • 模块化脚本架构,方便扩展

二、技术选型

2.1 模板引擎:Pug

Hexo 主题支持 PugEJS 两种模板引擎。选择 Pug 的原因:

  • 缩进语法:嵌套结构一目了然,不需要成对的 <% %> 标签
  • 原生支持extends/block/include/mixin 等模板功能
  • 与 Stylus 一致:同为简洁表达力强的设计哲学
pug
1
2
3
4
5
6
// Pug 示例:条件渲染 + 循环 if is_post() .post-item-header-meta each tag in post.tags.data .post-item-header-meta-item a(href=url_for(tag.path))= tag.name

Fan 主题迁移到 Pug 的过程中,Hexo 版本升级没有遇到严重的不兼容问题。主要是一些小的加载逻辑 bug,比如模板继承顺序、变量作用域等,调试后都能解决。Pug 本身在 Hexo 各版本间保持了良好的向后兼容性。

2.2 样式预处理:Stylus

Stylus 的缩进语法与 Pug 一致,支持变量、Mixin、函数,适合构建设计令牌系统:

stylus
1
2
3
4
// 设计令牌:所有值通过 theme-config() 从配置文件读取 $color-theme = theme-config('style.color.theme', 'rgba(230, 119, 0, 1)'); $color-background = theme-config('style.color.background', 'radial-gradient(100% 100% at 70% 120%, rgba(33, 39, 80, 1) 10%, #020409 100%)');

2.3 客户端脚本:ES6 Modules

所有客户端脚本使用 ES6 Modules,浏览器原生支持,无需构建步骤:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
// source/js/main.js — 入口文件 import { initClock } from "./layout/header.js"; import { initToggleSidebar } from "./layout/sidebar.js"; import ScrollHandler from "./utils/scroll.js"; import Background from "./layout/background.js"; document.addEventListener("DOMContentLoaded", () => { initClock(); initToggleSidebar(); new ScrollHandler(); // 有状态组件用 class new Background(); // Canvas 动画 });

两种初始化模式并存:无状态工具用 init*() 函数,有状态组件用 new Class() 实例化。

三、三层配置合并与加载调试

这是 DoraTiger 最核心的架构设计。Hexo 主题的配置管理一直是个痛点 — 用户需要在主题的 _config.yml 中修改配置,但主题升级时这个文件会被覆盖。三层合并机制解决了这个问题:

text
1
2
3
4
优先级(高 → 低): _config.hexo-theme-doratiger.yml ← 用户覆盖(推荐) source/_data/doratiger_config.yml ← 已废弃 themes/xxx/_config.yml ← 主题默认(993 行)

3.1 合并实现

javascript
1
2
3
4
5
6
7
8
// scripts/events/lib/themeConfig.js — 核心合并逻辑 themeMergeConfig = merge({}, defaultThemeConfig); // 层 1:主题默认值 if (isNotEmptyObject(dataThemeConfig)) { themeMergeConfig = merge({}, themeMergeConfig, dataThemeConfig); // 层 2:用户数据 } if (isNotEmptyObject(rootThemeConfig)) { themeMergeConfig = merge({}, themeMergeConfig, rootThemeConfig); // 层 3:根目录覆盖(最高优先级) }

使用深度合并(deep merge),嵌套对象不会被整体替换,而是逐字段合并。

3.2 调试过程中的最大坑:配置加载时序

最初实现三层合并时遇到了一个很大的问题:多处配置的加载顺序不一致Hexo 的生命周期中,配置在不同阶段被读取,但有些模块需要的环境变量在配置还没加载完的时候就被引用了。结果就是部分功能的配置项读不到值,表现为"配置写了但不生效"。

排查后发现根本原因是:Hexoready 事件、generateBefore 事件、模板渲染阶段各自读取配置的时机不同,如果没有统一的加载器,就很容易出现时序问题。

最终参考了其他主题的实现思路,完全自行构建了变量加载器,在 ready 事件中一次性完成所有配置的读取和合并,然后在 generateBefore 阶段统一写回。这样无论后续哪个模块读取配置,拿到的都是完整且正确的值。

text
1
2
3
4
5
6
修复前: ready → 部分模块读配置 → generateBefore → 其他模块读配置(不一致) 修复后: ready → themeConfig.js 统一加载三层配置 → mergeConfig.js 一次性写回 generateBefore → 所有模块读到的配置一致

3.3 两步模式的优势

javascript
1
2
3
// scripts/events/lib/mergeConfig.js hexo.theme.config = merge({}, themeConfig, doratiger.config); hexo.theme.i18n.data = merge({}, themeI18nConfig.data, doratiger.i18n.data);

这种两步模式(themeConfig 收集 → mergeConfig 应用)将配置解析与 Hexo 内部生命周期解耦。配置的"计算"和"生效"分离,便于调试和扩展。

四、脚本注入架构

4.1 Hook 的生效机制

Hexoinjector 系统提供四个生命周期钩子:head_beginhead_endbody_beginbody_end。理解这些钩子的生效时机是正确注入内容的关键:

text
1
2
3
4
5
6
7
8
9
10
HTML 渲染顺序: <head> head_begin ← 非常早期,DOM 还没构建 head_end ← head 尾部,适合 CSS 和配置脚本 </head> <body> body_begin ← body 开头 [页面内容] body_end ← body 尾部,适合 JS 和评论初始化 </body>

4.2 空 hook 的意义

DoraTiger 中有些 hook 当前是空的(如 head_begin),但仍然注册了默认内容。这是有意为之 — 为将来可能的功能预留注入点,避免后续添加功能时需要修改核心注入逻辑:

javascript
1
2
3
4
// scripts/injectors/index.js // 即使当前为空,也保持注册 hexo.extend.injector.register("head_begin", () => {}, "default"); hexo.extend.injector.register("body_begin", () => {}, "default");

4.3 实际注入内容

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// head_end:注入 CSS + 搜索配置 hexo.extend.injector.register("head_end", function () { let inject_content = []; inject_content.push(require("./lib/injector-config.js")(hexo)); inject_content.push(require("./lib/injector-search.js")(hexo)); inject_content.push(require("./lib/injector-resource.js")(hexo, "css")); return inject_content.join("\n"); }, "default"); // body_end:注入 JS + 评论 + 统计 hexo.extend.injector.register("body_end", () => { let inject_content = []; inject_content.push(require("./lib/injector-resource.js")(hexo, "js")); inject_content.push(require("./lib/injector-resource.js")(hexo, "script")); inject_content.push(require("./lib/injector-comments.js")(hexo)); return inject_content.join("\n"); }, "default");

4.4 资源加载器的门控机制

资源注入按功能开关控制 — 只有启用的功能才注入对应资源:

javascript
1
2
3
4
5
6
7
8
// scripts/injectors/lib/injector-resource.js const search = theme.search || {}; if (search.enable && search_type) { resources.push(loadResource(resource[search_type], resourceType, globalCDN)); } if (statistics.enable) { resources.push(loadResource(resource[statistics_type], resourceType, globalCDN)); }

五、内置第三方库的设计决策

DoraTigerhighlight.jsmathjaxfont-awesome 等第三方库内置在 source/lib/ 下,而非通过 CDN 引用。这个决策背后有两个实际原因:

国内网络问题:部分 CDN 在国内访问不稳定,jsDelivr 等偶尔会被限速或不可达。内置库可以确保加载成功率。

内网开发需求:写博客时经常处于内网环境,没有外网连接。如果依赖 CDN,本地预览时样式和功能都会缺失。内置库让离线开发成为可能。

为此设计了本地 + CDN 双重机制,每个资源都可以独立切换:

yaml
1
2
3
4
5
6
resource: enable_cdn: false # 全局默认:本地 highlight: enable_cdn: true # 代码高亮强制 CDN(库体积大) mathjax: enable_cdn: false # 数学渲染用本地(稳定性优先)
text
1
2
3
4
5
6
7
8
9
source/lib/ 内置库清单: ├── highlight.js/@11.10.0/ # 代码高亮 ├── mathjax/@3.2.2/ # 数学渲染 ├── font-awesome/@6.7.2/ # 图标字体 ├── twikoo/@1.6.40/ # 评论系统 ├── valine/@1.5.3/ # 评论系统 ├── instantsearch.js/ # Algolia 搜索 UI ├── pretext/ # Canvas 文字排版 └── prism.js/@1.29.0/ # 备用代码高亮

六、目录结构设计

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
themes/hexo-theme-doratiger/ ├── layout/ # 模板层 │ ├── *.pug # 页面模板(index/post/archive...) │ └── _include/ # 可复用组件 │ ├── _layout.pug # 主布局骨架 │ ├── head.pug # SEO/OG/JSON-LD │ ├── header.pug # 导航栏 │ ├── footer.pug # 底栏 + 统计 + 备案 │ └── sidebar.pug # 侧边栏路由 ├── scripts/ # 服务端脚本层 │ ├── events/ # 生命周期钩子 │ ├── filters/ # 内容过滤器 │ ├── generators/ # 页面生成器 │ ├── injectors/ # 资源注入器 │ └── console/ # CLI 命令 ├── source/ # 静态资源层 │ ├── css/ # Stylus 样式 │ ├── js/ # ES6 客户端脚本 │ └── lib/ # 内置第三方库 └── languages/ # i18n 翻译

关键设计决策:

决策 选择 原因
模板引擎 Pug 缩进语法,嵌套清晰
样式预处理 Stylus 与 Pug 设计哲学一致
JS 模块化 ES6 Modules 浏览器原生支持,无构建步骤
第三方库 内置 source/lib/ 国网不稳定 + 内网离线需求
配置方式 三层合并 + 统一加载器 解决多处配置加载时序问题
主题色 深蓝黑 + 橙色 暗色为主,橙色强调
Hook 注册 全部注册(含空 hook) 预留扩展点,保持架构一致

七、总结

DoraTiger 的架构核心是三层配置合并 + 统一加载器模块化脚本注入。前者解决了配置加载时序的痛点,后者让功能扩展变得清晰可控。一体化集成的设计选择让加密、搜索、统计等功能可以共享配置体系和生命周期管理,而内置第三方库则确保了在各种网络环境下的可用性。下一篇将深入视觉系统的设计实现。

参考

本文作者: 有次元袋的 tiger
本文链接: https://www.superheaoz.top/2026/06/13838/
版权声明: 本站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 我的个人天地
手机扫码阅读