引言

用Hexo写博客已经有一段时间了,每次执行hexo generate命令看着终端刷刷刷地输出”Generated: xxx.html”,我就在想:这玩意儿到底是怎么把我写的Markdown变成网页的?

于是某天晚上,我打开了node_modules/hexo目录,开始翻源代码。不看不知道,一看发现Hexo的设计还挺有意思的——生成器、渲染器、过滤器、路由…这些概念组合起来,形成了一套完整的渲染流程。

这篇文章记录了我分析Hexo 8.0.0源码的过程,顺便也研究了一下Butterfly 5.5.1主题是怎么配合Hexo工作的。如果你也好奇Hexo的实现原理,或者想自己开发主题/插件,希望这篇文章能帮到你。

Hexo渲染概述

什么是渲染?先来看个例子

假设你写了这样一个Markdown文件(source/_posts/hello.md):

1
2
3
4
5
6
7
8
9
10
11
12
---
title: 我的第一篇博客
date: 2025-10-21
tags: [技术, Hexo]
---

## 欢迎

这是我的第一篇博客文章,使用**Hexo**搭建。

```js
console.log('Hello Hexo!');

当你执行hexo generate后,Hexo会生成一个public/2025/10/21/hello/index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<title>我的第一篇博客</title>
<!-- 各种CSS、meta标签 -->
</head>
<body>
<nav>导航栏...</nav>
<article>
<h1>我的第一篇博客</h1>
<div class="post-meta">
<time>2025-10-21</time>
<span>标签: 技术, Hexo</span>
</div>
<h2>欢迎</h2>
<p>这是我的第一篇博客文章,使用<strong>Hexo</strong>搭建。</p>
<pre><code class="language-js">console.log('Hello Hexo!');</code></pre>
</article>
<footer>页脚...</footer>
</body>
</html>

这个过程就是渲染。它包含三个层次的转换:

  1. 内容渲染:Markdown → HTML片段

    • ## 欢迎<h2>欢迎</h2>
    • **Hexo**<strong>Hexo</strong>
  2. 模板渲染:数据 + 模板 → 完整HTML

    • 把文章内容、标题、日期等数据
    • 注入到主题的layout模板中
    • 生成包含导航栏、页脚的完整页面
  3. 资源处理:编译样式和脚本

    • Stylus → CSS
    • 压缩、优化静态资源

Hexo的技术栈

基于源码分析,Hexo 8.0.0采用了以下核心技术:

核心依赖

  • Nunjucks:默认模板引擎(同时支持EJS、Pug等)
  • Marked:Markdown渲染器
  • Warehouse:轻量级数据库,用于管理文章、标签、分类等数据
  • Bluebird:Promise增强库,用于异步流程控制

Butterfly主题技术栈

  • Pug:模板引擎(而非EJS)
  • Stylus:CSS预处理器

Hexo核心渲染流程分析

整体架构

Hexo的渲染系统是一个精心设计的插件化架构,核心文件位于node_modules/hexo/dist/目录下。让我们从hexo generate命令开始,追踪整个渲染流程。

核心类结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hexo/dist/hexo/index.js (主类)
class Hexo extends EventEmitter {
constructor(base = process.cwd(), args = {}) {
this.extend = {
console: new Console(),
generator: new Generator(),
filter: new Filter(),
renderer: new Renderer(),
helper: new Helper(),
// ... 更多扩展
};
this.render = new Render(this);
this.route = new Router();
this.theme = new Theme(this);
}
}

关键组件说明

  • extend.generator:负责生成路由和页面数据
  • extend.renderer:管理各种文件格式的渲染器
  • extend.filter:提供before/after钩子,用于内容处理
  • render:渲染引擎实例
  • route:路由管理器,存储所有待生成的页面
  • theme:主题管理器

渲染流程五步曲

接下来,我们用上面的hello.md作为例子,跟踪它在Hexo内部的完整渲染过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
用户执行: hexo generate

[第一步] 初始化与加载

[第二步] 运行生成器

[第三步] 刷新路由

[第四步] 执行渲染

[第五步] 写入文件

生成: public/2025/10/21/hello/index.html

第一步:初始化与加载(init & load)

这一步做什么?
把博客的所有原材料准备好——扫描你的Markdown文件、主题模板、配置等。

具体过程:

1
2
3
4
5
6
// 入口:hexo/dist/plugins/console/generate.js
function generateConsole(args = {}) {
const generator = new Generator(this, args);
return this.load() // 开始加载
.then(() => generator.firstGenerate());
}

当执行this.load()时(hexo/dist/hexo/index.js:270):

1
2
3
4
5
6
7
8
9
10
11
load(callback) {
return loadDatabase(this).then(() => {
return Promise.all([
this.source.process(), // 扫描source目录
this.theme.process() // 扫描主题文件
]);
}).then(() => {
mergeCtxThemeConfig(this);
return this._generate({ cache: false });
});
}

实际发生了什么:

  1. 加载数据库 (db.json)

    1
    2
    3
    4
    数据库里存储着:
    - 所有文章的元数据(标题、日期、标签等)
    - 分类、标签的索引
    - 上次生成的文件hash(用于判断是否需要重新生成)
  2. 扫描source目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    发现文件: source/_posts/hello.md

    读取front-matter:
    {
    title: "我的第一篇博客",
    date: 2025-10-21,
    tags: ["技术", "Hexo"]
    }

    读取正文内容(Markdown)

    存入数据库: Post集合
  3. 扫描主题模板

    1
    2
    3
    4
    5
    6
    7
    发现模板文件:
    - themes/butterfly/layout/post.pug
    - themes/butterfly/layout/index.pug
    - themes/butterfly/layout/includes/layout.pug
    ...

    预编译Pug模板 → 函数(待后续调用)
  4. 合并主题配置

    1
    2
    合并 _config.yml 和 _config.butterfly.yml
    最终配置存入 hexo.theme.config

此时的状态:

  • Hexo已经知道你有一篇hello.md文章
  • 已经把Markdown内容读取到内存
  • 主题模板已经预编译好
  • 但还没有生成任何HTML

第二步:运行生成器(_runGenerators)

这一步做什么?
决定要生成哪些页面,每个页面用什么模板、包含什么数据。

具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
// hexo/dist/hexo/index.js:348
_runGenerators() {
this.locals.invalidate();
const siteLocals = this.locals.toObject(); // 获取所有文章、标签等数据
const generators = this.extend.generator.list(); // 获取所有生成器

return Promise.map(Object.keys(generators), key => {
const generator = generators[key];
return generator.call(this, siteLocals); // 调用每个生成器
}).reduce((result, data) => {
return data ? result.concat(data) : result;
}, []);
}

Hexo内置的生成器:

  1. post生成器:为每篇文章生成页面
  2. index生成器:生成首页(可能有分页)
  3. archive生成器:生成归档页
  4. category/tag生成器:生成分类/标签页

对于我们的hello.md,post生成器会返回:

1
2
3
4
5
6
7
8
9
10
11
{
path: '2025/10/21/hello/index.html', // 生成的文件路径
data: { // 页面数据
title: '我的第一篇博客',
date: Mon Oct 21 2025...,
tags: [{name: '技术', ...}, {name: 'Hexo', ...}],
content: '## 欢迎\n\n这是我的第一篇...', // 原始Markdown
_content: '## 欢迎\n\n这是我的第一篇...'
},
layout: ['post', 'page', 'index'] // 模板优先级列表
}

生成器为什么返回layout数组?
这是一个fallback机制。Hexo会按顺序查找模板:

  • 先找post.pug → 找到了,用它!
  • 如果没找到,找page.pug
  • 还没有?找index.pug
  • 都没有?报错

此时的状态:

  • Hexo已经知道要生成2025/10/21/hello/index.html
  • 知道要用post.pug模板
  • 知道要传入的数据(标题、日期、Markdown内容)
  • 但Markdown还没有转成HTML,模板也还没渲染

第三步:刷新路由(_routerRefresh)

这一步做什么?
把上一步生成器返回的信息,注册成”路由”。可以理解为给每个页面设置一个”快捷方式”。

关键点:这一步不会真正渲染!只是准备好渲染函数,等到需要的时候再调用(惰性渲染)。

具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// hexo/dist/hexo/index.js:362
_routerRefresh(runningGenerators, useCache) {
const Locals = this._generateLocals();

return runningGenerators.map(generatorResult => {
const path = route.format(generatorResult.path);
const { data, layout } = generatorResult;

if (!layout) {
// 没有layout说明是静态资源,直接设置
route.set(path, data);
return path;
}

// 创建locals对象(包含page、config、theme等)
return this.execFilter('template_locals', new Locals(path, data))
.then(locals => {
// 注册路由,但只是存储渲染函数
route.set(path, createLoadThemeRoute(generatorResult, locals, this));
});
});
}

对于hello.md,会创建这样一个路由:

1
2
3
4
5
route.set('2025/10/21/hello/index.html', function() {
// 这个函数现在还不会执行!
// 它会在第五步写文件时才被调用
return 渲染post.pug模板(locals);
});

Locals对象包含什么?
这是传给模板的所有数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
page: { // 当前页面数据
title: '我的第一篇博客',
date: ...,
tags: [...],
content: '## 欢迎\n\n...' // 还是Markdown
},
config: { // 全局配置
title: '我的博客',
url: 'https://example.com',
...
},
theme: { // 主题配置
...
},
site: { // 整个网站数据
posts: [...], // 所有文章
tags: [...], // 所有标签
categories: [...] // 所有分类
},
url: 'https://example.com/2025/10/21/hello/',
path: '2025/10/21/hello/index.html',
// 还有各种Helper函数
url_for: function() {...},
date: function() {...},
...
}

此时的状态:

  • 路由已经注册:route.get('2025/10/21/hello/index.html') 可以获取渲染函数
  • 但渲染函数还没执行
  • Markdown还是原样

第四步:执行渲染(createLoadThemeRoute)

这一步做什么?
当路由被调用时(route.get(path)),开始真正的渲染工作。

渲染函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// hexo/dist/hexo/index.js:45
const createLoadThemeRoute = function(generatorResult, locals, ctx) {
const { theme } = ctx;
const { path } = locals;
const layout = [...new Set(castArray(generatorResult.layout))];

return () => { // 返回一个函数(还没执行)
// 按优先级查找layout
for (let i = 0; i < layout.length; i++) {
const view = theme.getView(layout[i]);
if (view) {
ctx.log.debug(`Rendering HTML ${layout[i]}: ${path}`);

// 这里才真正开始渲染!
return view.render(locals)
.then(result => ctx.extend.injector.exec(result, locals))
.then(result => ctx.execFilter('_after_html_render', result, {
context: ctx,
args: [locals]
}));
}
}
ctx.log.warn(`No layout: ${path}`);
};
}

实际渲染过程(以hello.md为例):

  1. 查找模板

    1
    2
    尝试 layout[0] = 'post'
    theme.getView('post') → 找到 themes/butterfly/layout/post.pug
  2. 渲染前:Markdown转HTML(过滤器)

    1
    2
    3
    4
    5
    6
    // 在before_post_render过滤器中
    page.content = marked(page._content);
    // 结果:
    // '## 欢迎\n\n这是...'
    // →
    // '<h2>欢迎</h2>\n<p>这是...</p>'
  3. 渲染模板(view.render)

    post.pug模板大概长这样:

    1
    2
    3
    4
    5
    6
    extends includes/layout.pug

    block content
    #post
    article#article-container.post-content
    != page.content

    第一次渲染post.pug:

    1
    2
    3
    4
    5
    6
    7
    <div id="post">
    <article id="article-container" class="post-content">
    <h2>欢迎</h2>
    <p>这是我的第一篇博客文章,使用<strong>Hexo</strong>搭建。</p>
    <pre><code class="language-js">console.log('Hello Hexo!');</code></pre>
    </article>
    </div>
  4. 递归渲染layout.pug

    因为post.pug extends includes/layout.pug,所以会继续渲染layout.pug:

    layout.pug大概长这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    doctype html
    html
    head
    include ./head.pug
    body
    include ./header/index.pug
    main#content-inner
    block content
    include ./footer.pug

    最终渲染结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html>
    <html>
    <head>
    <title>我的第一篇博客</title>
    <link rel="stylesheet" href="/css/index.css">
    ...
    </head>
    <body>
    <nav>导航栏...</nav>
    <main id="content-inner">
    <!-- 这里插入post.pug渲染的内容 -->
    <div id="post">
    <article id="article-container" class="post-content">
    <h2>欢迎</h2>
    <p>这是我的第一篇博客文章...</p>
    </article>
    </div>
    </main>
    <footer>页脚...</footer>
    </body>
    </html>
  5. Injector注入

    1
    2
    // 在</head>前注入额外的CSS
    // 在</body>前注入额外的JS
  6. after_html_render过滤器

    1
    // 可能做HTML压缩、添加统计代码等

此时的状态:

  • 完整的HTML字符串已经生成
  • 但还没有写入文件

第五步:写入文件(writeFile)

这一步做什么?
把渲染好的HTML写入public目录,并做一些优化(如检查文件是否变化)。

具体过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// hexo/dist/plugins/console/generate.js:52
writeFile(path, force) {
const { route } = this.context;

// 调用route.get(path),触发第四步的渲染
const dataStream = this.wrapDataStream(route.get(path));
const buffers = [];
const hasher = createSha1Hash();

// 收集渲染结果
dataStream.on('data', chunk => {
buffers.push(chunk);
hasher.update(chunk); // 计算hash
});

return finishedPromise.then(() => {
const dest = join(publicDir, path); // public/2025/10/21/hello/index.html
const hash = hasher.digest('hex');

// 检查文件是否变化
if (!force && cache && cache.hash === hash) {
return; // 内容没变,跳过写入
}

// 保存hash到缓存
Cache.save({ _id: `public/${path}`, hash });

// 写入文件
return writeFile(dest, Buffer.concat(buffers))
.then(() => {
log.info('Generated: %s', path);
});
});
}

实际发生了什么:

  1. 触发渲染

    1
    2
    3
    route.get('2025/10/21/hello/index.html')
    → 执行第四步创建的渲染函数
    → 返回完整HTML字符串
  2. 计算hash

    1
    2
    对HTML内容计算SHA1哈希值
    例如: a3f5e8c9b2d1...
  3. 检查缓存

    1
    2
    3
    4
    查看db.json中是否有这个文件的记录
    对比hash值
    - 如果相同:跳过写入(节省时间)
    - 如果不同:继续写入
  4. 创建目录并写入

    1
    2
    mkdir -p public/2025/10/21/hello/
    写入 index.html
  5. 终端输出

    1
    INFO  Generated: 2025/10/21/hello/index.html

最终结果:

1
2
3
4
5
6
public/
└── 2025/
└── 10/
└── 21/
└── hello/
└── index.html ← 完整的HTML页面

整个流程总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hello.md (Markdown)
↓ [第一步] 扫描、解析front-matter

存入数据库 (Post对象)
↓ [第二步] post生成器

生成器结果 {path, data, layout}
↓ [第三步] 注册路由

route.set(path, 渲染函数)
↓ [第五步] 写文件时调用route.get(path)
↓ [第四步] 执行渲染函数
↓ - Markdown → HTML (marked)
↓ - 渲染post.pug
↓ - 递归渲染layout.pug
↓ - Injector注入
↓ - 过滤器处理

完整HTML字符串
↓ [第五步] 写入文件

public/2025/10/21/hello/index.html

View渲染机制深入

View类(hexo/dist/theme/view.js)是模板渲染的核心,负责将模板编译并执行。

什么是View?

每个模板文件都对应一个View对象。比如themes/butterfly/layout/post.pug会创建一个View实例。

1
2
3
4
5
// 创建View(在主题加载时)
const view = new View('post.pug', pugFileContent);

// 渲染View(在生成HTML时)
const html = await view.render(locals);

View的预编译机制

为什么要预编译?
如果每次渲染都重新解析Pug模板,会很慢。所以Hexo在加载主题时就把模板编译成函数,渲染时直接调用函数。

预编译过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// hexo/dist/theme/view.js:94
_precompile() {
const ext = extname(this.path); // 获取扩展名(.pug)
const renderer = this._render.getRenderer(ext); // 获取Pug渲染器
const data = {
path: this.source,
text: this.data._content // 模板源代码
};

if (renderer && typeof renderer.compile === 'function') {
// 编译模板 → 函数
const compiled = renderer.compile(data);

// 保存编译后的函数
this._compiled = locals =>
Promise.resolve(compiled(locals))
.then(result => ctx.execFilter(...buildFilterArguments(result)));
} else {
// 不支持预编译(如Markdown),每次都渲染
this._compiled = locals => this._render.render(data, locals);
}
}

举个例子:

1
2
3
// post.pug 源代码
h1= page.title
p= page.date

编译后变成类似这样的函数:

1
2
3
4
function compiled(locals) {
return '<h1>' + locals.page.title + '</h1>' +
'<p>' + locals.page.date + '</p>';
}

下次渲染时,直接调用compiled(locals),非常快!

View的递归渲染(Layout继承)

这是Hexo最巧妙的设计之一。让我们通过例子理解:

post.pug(子模板):

1
2
3
4
5
6
extends includes/layout.pug

block content
#post
h1= page.title
!= page.content

includes/layout.pug(父模板):

1
2
3
4
5
6
7
8
9
doctype html
html
head
title= page.title
body
nav 这是导航栏
main
block content
footer 这是页脚

渲染过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// hexo/dist/theme/view.js:29
render(options = {}, callback) {
const { layout } = this.data; // 从front-matter读取layout
const locals = this._buildLocals(options);

// 第一次:渲染post.pug
return this._compiled(this._bindHelpers(locals))
.then(result => {
// result现在是:
// <div id="post"><h1>...</h1>...</div>

if (result == null || !layout) return result;

// 找到父模板(layout.pug)
const layoutView = this._resolveLayout(layout);
if (!layoutView) return result;

// 准备传给父模板的数据
const layoutLocals = {
...locals,
body: result, // 把刚才渲染的结果作为body
layout: false // 防止无限递归
};

// 第二次:递归渲染layout.pug
return layoutView.render(layoutLocals, callback);
});
}

最终结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
<title>我的第一篇博客</title>
</head>
<body>
<nav>这是导航栏</nav>
<main>
<!-- 这里插入post.pug的渲染结果(body) -->
<div id="post">
<h1>我的第一篇博客</h1>
<h2>欢迎</h2>
<p>这是我的第一篇博客...</p>
</div>
</main>
<footer>这是页脚</footer>
</body>
</html>

关键点:

  1. block content是一个”占位符”
  2. post.pug的内容会填充到这个占位符
  3. 通过body变量传递渲染结果
  4. 设置layout: false防止无限递归

这种递归机制让Butterfly可以实现复杂的布局嵌套(post.pug → layout.pug → 可能还有更上层的layout)。

Butterfly主题渲染机制

Butterfly作为最流行的Hexo主题之一,其渲染机制在Hexo核心之上构建了精巧的模板体系。

Butterfly的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
hexo-theme-butterfly/
├── layout/ # 布局模板
│ ├── includes/ # 可复用组件
│ │ ├── layout.pug # 基础布局
│ │ ├── head.pug # <head>部分
│ │ ├── header/ # 顶部导航
│ │ ├── post/ # 文章相关
│ │ ├── widget/ # 侧边栏小部件
│ │ └── mixins/ # Pug mixins
│ ├── index.pug # 首页
│ ├── post.pug # 文章页
│ ├── page.pug # 独立页面
│ └── archive.pug # 归档页
├── source/ # 静态资源
│ ├── css/
│ └── js/
└── scripts/ # 主题脚本(Hexo插件)

Butterfly的渲染流程

1. 主题配置合并

Butterfly支持两种配置文件:

  • themes/butterfly/_config.yml(默认配置)
  • _config.butterfly.yml(用户配置,优先级更高)

合并逻辑在hexo/dist/hexo/index.js:37

1
2
3
4
5
const mergeCtxThemeConfig = (ctx) => {
if (ctx.config.theme_config) {
ctx.theme.config = deepMerge(ctx.theme.config, ctx.config.theme_config);
}
};

2. Pug渲染器工作原理

Butterfly使用Pug(前身Jade)作为模板引擎,需要hexo-renderer-pug插件。

Pug特性

  • 简洁语法:基于缩进,无需闭合标签
  • extends/include:支持模板继承和包含
  • Mixins:可复用的模板片段
  • 内联JavaScript:使用-前缀执行JS代码

示例layout/post.pug):

1
2
3
4
5
6
7
8
9
extends includes/layout.pug

block content
#post
article#article-container.container.post-content
if theme.noticeOutdate.enable && page.noticeOutdate !== false
include includes/post/outdate-notice.pug
else
!=page.content

3. Helper函数注入

在View渲染时,Hexo会将所有Helper函数注入到localshexo/dist/theme/view.js:75):

1
2
3
4
5
6
7
_bindHelpers(locals) {
const helpers = this._helper.list();
for (const key of Object.keys(helpers)) {
locals[key] = helpers[key].bind(locals);
}
return locals;
}

Butterfly常用Helper

  • url_for(path):生成URL
  • partial(template, locals):渲染partial
  • page_nav():分页导航
  • date()time():日期时间格式化

4. Butterfly的自定义Scripts

Butterfly在scripts/目录下扩展了Hexo功能(通过Hexo的插件系统):

1
2
3
4
5
6
7
// scripts/helpers/page.js
hexo.extend.helper.register('getPageType', function(page, is_home) {
if (is_home) return 'index';
if (page.type === 'tags') return 'tag';
if (page.type === 'categories') return 'category';
// ...
});

这些Helper在模板中直接调用:

1
- var globalPageType = getPageType(page, is_home)

典型页面渲染流程示例

以文章页为例,完整流程:

  1. 生成器阶段post生成器为每篇文章创建路由

    1
    2
    3
    4
    5
    {
    path: '2025/10/20/my-post/',
    layout: ['post', 'page', 'index'],
    data: { title: '...', content: '...', ... }
    }
  2. 模板查找:按优先级查找模板

    • layout/post.pug 找到
  3. Markdown渲染:在before_post_render过滤器中,将Markdown转为HTML

    1
    page.content = marked(page._content);
  4. Pug编译

    1
    2
    compiled = pug.compile(template);
    result = compiled({ page, config, theme, ... });
  5. Layout继承post.pug extends includes/layout.pug

    • 先渲染post.pugcontent
    • 将结果插入layout.pugblock content位置
  6. Injector注入:在</head>前注入CSS,在</body>前注入JS

  7. 写入文件:保存到public/2025/10/20/my-post/index.html

渲染器系统与过滤器

渲染器注册机制

Hexo通过extend.renderer管理所有渲染器(hexo/dist/extend/renderer.js):

1
2
3
4
// 注册Markdown渲染器
hexo.extend.renderer.register('md', 'html', function(data, options) {
return marked(data.text);
}, true); // true表示支持同步渲染

注册参数

  • 第一个参数:输入扩展名(如’md’)
  • 第二个参数:输出扩展名(如’html’)
  • 第三个参数:渲染函数
  • 第四个参数:是否支持同步

过滤器系统

Hexo的过滤器系统提供了强大的内容处理能力,关键过滤器包括:

渲染相关过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// before_post_render: Markdown渲染前
hexo.extend.filter.register('before_post_render', function(data) {
// 处理代码块、标题等
return data;
});

// after_post_render: Markdown渲染后,模板渲染前
hexo.extend.filter.register('after_post_render', function(data) {
// 添加excerpt、处理外链等
return data;
});

// after_render:html: HTML生成后
hexo.extend.filter.register('after_render:html', function(str, data) {
// 压缩HTML、注入代码等
return str;
});

过滤器执行时机

1
2
3
4
5
6
7
8
9
10
11
12
13
Markdown文件

before_post_render (处理原始Markdown)

Markdown渲染器 (marked)

after_post_render (处理HTML片段)

模板渲染 (Pug)

after_render:html (处理完整HTML)

写入public目录

性能优化机制

1. 路由缓存

Hexo使用WeakMap缓存已渲染的路由(hexo/dist/hexo/index.js:34):

1
2
3
4
5
6
7
const routeCache = new WeakMap();

if (useCache && routeCache.has(generatorResult)) {
return routeCache.get(generatorResult);
}
// ... 渲染逻辑
routeCache.set(generatorResult, result);

启用条件hexo server命令且配置server.cache: true

2. 文件Hash检测

避免重复写入未修改的文件(hexo/dist/plugins/console/generate.js:68):

1
2
3
4
const hash = hasher.digest('hex');
if (!force && cache && cache.hash === hash) {
return; // 跳过写入
}

3. 模板预编译

支持预编译的渲染器(Pug、EJS)会在主题加载时编译模板,渲染时直接执行编译后的函数,大幅提升性能。

总结与最佳实践

核心要点回顾

  1. 生成器-路由-渲染:Hexo采用三阶段渲染架构

    • 生成器生成路由数据
    • 路由管理器存储渲染函数(惰性渲染)
    • 写入文件时才执行真正的渲染
  2. 插件化设计:所有功能都通过extend系统扩展

    • Renderer:文件格式转换
    • Generator:路由生成
    • Filter:内容处理钩子
    • Helper:模板辅助函数
  3. 主题渲染:基于View类的递归渲染

    • 支持Layout继承
    • Helper函数自动注入
    • 过滤器链处理

开发建议

主题开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 注册自定义Helper
hexo.extend.helper.register('myHelper', function(arg) {
// 返回处理结果
});

// 2. 使用过滤器扩展功能
hexo.extend.filter.register('after_post_render', function(data) {
// 修改data.content
return data;
});

// 3. 创建自定义生成器
hexo.extend.generator.register('myGenerator', function(locals) {
return {
path: 'my-page.html',
layout: 'custom',
data: { ... }
};
});

性能优化

  1. 启用hexo server的缓存:_config.yml中设置server.cache: true
  2. 合理使用partial(template, locals, {cache: true})缓存partial
  3. 避免在模板中进行复杂计算,使用Helper预处理
  4. 使用hexo generate --concurrency 4并行生成文件

调试技巧

1
2
3
4
5
6
7
8
# 开启调试模式,查看详细日志
hexo generate --debug

# 清除缓存重新生成
hexo clean && hexo generate

# 查看特定文件的渲染过程
DEBUG=hexo:* hexo generate


参考资料