前言
前几篇笔记介绍了 Hexo 的基础知识、插件体系和进阶功能。从本篇开始,进入 DoraTiger 自建主题的实战开发。本篇作为系列第四篇,聚焦主题的整体架构设计:从 Fan 主题贡献者到独立开发者的转变过程,技术选型背后的思考,以及核心的三层配置合并机制和脚本注入架构。
一、从贡献者到独立开发者
1.1 与 Fan 主题的渊源
我最初使用的是 Fan 主题,并且在 GitHub 上为其贡献过代码,算是 Fan 主题的贡献者之一。使用了相当长一段时间,整体体验不错。
但后来作者长期没有更新,而 Hexo 版本在不断迭代。随着 Hexo 7.x 的发布,一些 API 和行为发生了变化,Fan 主题逐渐出现了兼容性问题。加上我当时正好有空,想深入学习一下 Pug、Stylus、ES6 Modules 等前端技术,所以决定从头构建一个自己的主题。
1.2 为什么选择一体化集成
最终没有选择继续维护 Fan 的 fork,而是完全重写,主要考虑的是一体化集成的设计理念。Hexo 主题生态中有大量优秀的第三方插件(加密、搜索、sitemap、统计等),但在实际使用中,将这些功能分散在不同插件中会带来一些问题:
- 各插件的配置格式不统一,维护成本高
- 插件之间可能存在兼容性冲突
- 功能逻辑分散在多处,排查问题困难
- 升级
Hexo版本时,需要逐个检查插件兼容性
DoraTiger 主题选择将常用功能一体化集成到主题内部,方便统一处理逻辑,减少外部依赖带来的维护成本。所有集成的功能均在主题 README.md 中标注了原始来源和引用链接,确保开源精神。
1.3 重写目标
- 一体化集成 — 加密、搜索、
sitemap、统计、外链拦截等功能直接内置 - 暗色主题 +
Canvas动画 - 完整的配置文档(
docs/CONFIG.md,1051 行) - 模块化脚本架构,方便扩展
二、技术选型
2.1 模板引擎:Pug
Hexo 主题支持 Pug 和 EJS 两种模板引擎。选择 Pug 的原因:
- 缩进语法:嵌套结构一目了然,不需要成对的
<% %>标签 - 原生支持:
extends/block/include/mixin等模板功能 - 与 Stylus 一致:同为简洁表达力强的设计哲学
pug123456// 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、函数,适合构建设计令牌系统:
stylus1234// 设计令牌:所有值通过 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,浏览器原生支持,无需构建步骤:
javascript123456789101112// 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 中修改配置,但主题升级时这个文件会被覆盖。三层合并机制解决了这个问题:
text1234优先级(高 → 低): _config.hexo-theme-doratiger.yml ← 用户覆盖(推荐) source/_data/doratiger_config.yml ← 已废弃 themes/xxx/_config.yml ← 主题默认(993 行)
3.1 合并实现
javascript12345678// 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 的生命周期中,配置在不同阶段被读取,但有些模块需要的环境变量在配置还没加载完的时候就被引用了。结果就是部分功能的配置项读不到值,表现为"配置写了但不生效"。
排查后发现根本原因是:Hexo 的 ready 事件、generateBefore 事件、模板渲染阶段各自读取配置的时机不同,如果没有统一的加载器,就很容易出现时序问题。
最终参考了其他主题的实现思路,完全自行构建了变量加载器,在 ready 事件中一次性完成所有配置的读取和合并,然后在 generateBefore 阶段统一写回。这样无论后续哪个模块读取配置,拿到的都是完整且正确的值。
text123456修复前: ready → 部分模块读配置 → generateBefore → 其他模块读配置(不一致) 修复后: ready → themeConfig.js 统一加载三层配置 → mergeConfig.js 一次性写回 generateBefore → 所有模块读到的配置一致
3.3 两步模式的优势
javascript123// 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 的生效机制
Hexo 的 injector 系统提供四个生命周期钩子:head_begin、head_end、body_begin、body_end。理解这些钩子的生效时机是正确注入内容的关键:
text12345678910HTML 渲染顺序: <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),但仍然注册了默认内容。这是有意为之 — 为将来可能的功能预留注入点,避免后续添加功能时需要修改核心注入逻辑:
javascript1234// scripts/injectors/index.js // 即使当前为空,也保持注册 hexo.extend.injector.register("head_begin", () => {}, "default"); hexo.extend.injector.register("body_begin", () => {}, "default");
4.3 实际注入内容
javascript1234567891011121314151617// 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 资源加载器的门控机制
资源注入按功能开关控制 — 只有启用的功能才注入对应资源:
javascript12345678// 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)); }
五、内置第三方库的设计决策
DoraTiger 将 highlight.js、mathjax、font-awesome 等第三方库内置在 source/lib/ 下,而非通过 CDN 引用。这个决策背后有两个实际原因:
国内网络问题:部分 CDN 在国内访问不稳定,jsDelivr 等偶尔会被限速或不可达。内置库可以确保加载成功率。
内网开发需求:写博客时经常处于内网环境,没有外网连接。如果依赖 CDN,本地预览时样式和功能都会缺失。内置库让离线开发成为可能。
为此设计了本地 + CDN 双重机制,每个资源都可以独立切换:
yaml123456resource: enable_cdn: false # 全局默认:本地 highlight: enable_cdn: true # 代码高亮强制 CDN(库体积大) mathjax: enable_cdn: false # 数学渲染用本地(稳定性优先)
text123456789source/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/ # 备用代码高亮
六、目录结构设计
text1234567891011121314151617181920themes/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 的架构核心是三层配置合并 + 统一加载器和模块化脚本注入。前者解决了配置加载时序的痛点,后者让功能扩展变得清晰可控。一体化集成的设计选择让加密、搜索、统计等功能可以共享配置体系和生命周期管理,而内置第三方库则确保了在各种网络环境下的可用性。下一篇将深入视觉系统的设计实现。

