探索 hexo 生成器

Hexo 的 generators 是如何运作的

Posted by Light on 2024-05-25 | 2.2k words, 2 mins. |

hexo 是如何利用 theme 下的文件生成 html 页面的?
hexo-generator 是如何利用插件的?hexo-generator 生成的结果是什么数据?

为什么我的 archives/index.html 会利用 theme/layout/archive.ejs 生成?
为什么 tags/index.html 没有生成,/tags/tag-a.html 却生成了,并且利用了 theme/tag.ejs 这个模板?

通过源码探索

我们执行 hexo generate 的时候,依赖了博客目录下 node_modules/hexo 里面的代码,那么直接从依赖里面开找。

hexo/index.js 开始

生成网站 html 文件需要我们执行 hexo generate。所以直接从 generate 开始看,从 index.js 搜索到 _generate 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_generate(options = {}) {
if (this._isGenerating)
return;
const useCache = options.cache;
this._isGenerating = true;
this.emit('generateBefore');
// Run before_generate filters
return this.execFilter('before_generate', null, { context: this })
.then(() => this._routerRefresh(this._runGenerators(), useCache)).then(() => {
this.emit('generateAfter');
// Run after_generate filters
return this.execFilter('after_generate', null, { context: this });
}).finally(() => {
this._isGenerating = false;
});
}

这段代码中,我们主要关注this._runGenerators()this._routerRefresh 部分。

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
35
36
37
38
39
40
41
42
43
44
_runGenerators() {
this.locals.invalidate();
const siteLocals = this.locals.toObject();
const generators = this.extend.generator.list();
const { log } = this;
// Run generators
return bluebird_1.default.map(Object.keys(generators), key => {
const generator = generators[key];
log.debug('Generator: %s', (0, picocolors_1.magenta)(key));
return Reflect.apply(generator, this, [siteLocals]);
}).reduce((result, data) => {
return data ? result.concat(data) : result;
}, []);
}
_routerRefresh(runningGenerators, useCache) {
const { route } = this;
const routeList = route.list();
const Locals = this._generateLocals();
Locals.prototype.cache = useCache;
return runningGenerators.map((generatorResult) => {
if (typeof generatorResult !== 'object' || generatorResult.path == null)
return undefined;
// add Route
const path = route.format(generatorResult.path);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { data, layout } = generatorResult;
if (!layout) {
route.set(path, data);
return path;
}
return this.execFilter('template_locals', new Locals(path, data), { context: this })
.then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
.thenReturn(path);
}).then(newRouteList => {
// Remove old routes
for (let i = 0, len = routeList.length; i < len; i++) {
const item = routeList[i];
if (!newRouteList.includes(item)) {
route.remove(item);
}
}
});
}

我们在 _runGenerators 中可以看到,它将 generator 这个方法类型的变量进行执行,并将返回的结果整合到一起返回。

然后在 _routerRefresh 会对每一个 generatorResult 判断它的三个属性 path, layoutdata
我们可以得知,每一个 generator 执行后,返回的结果结构就会包含这三个字段。

接下来我们关注 createLoadThemeRoute() 方法,这个方法就是对应通过 theme 生成 html 的逻辑。

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
35
36
37
38
39
40
const createLoadThemeRoute = function (generatorResult, locals, ctx) {
const { log, theme } = ctx;
const { path, cache: useCache } = locals;
const layout = [...new Set(castArray(generatorResult.layout))];
const layoutLength = layout.length;
// always use cache in fragment_cache
locals.cache = true;
return () => {
if (useCache && routeCache.has(generatorResult))
return routeCache.get(generatorResult);
for (let i = 0; i < layoutLength; i++) {
const name = layout[i];
const view = theme.getView(name);
if (view) {
log.debug(`Rendering HTML ${name}: ${(0, picocolors_1.magenta)(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],
})
)
.tap((result) => {
if (useCache) {
routeCache.set(generatorResult, result);
}
})
.tapCatch((err) => {
log.error(
{ err },
`Render HTML failed: ${(0, picocolors_1.magenta)(path)}`
);
});
}
}
log.warn(`No layout: ${(0, picocolors_1.magenta)(path)}`);
};
};

我们可以看到,这里通过遍历 generatorResultlayout 字段,这个数组字段,从 theme/layout 文件夹下面匹配我们编写的模板文件。
同时,因为 for 循环中使用了 return,所以会按照 generatorResult.layout 字段数组顺序,在 theme/layout 文件夹下找对应的模板文件,找到第一个就返回。

再看 hexo 中定义的 generater

我们知道创建 generator 的时候,需要在 index.js 中编写以下代码:

hexo.extend.generator.register('', fn);

所以我们直接去 hexo 的 extend 文件夹中寻找 generator 相关的定义:

extend/generator.d.ts
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
import Promise from "bluebird";
import type { NodeJSLikeCallback } from "../types";
interface BaseObj {
path: string;
data: any;
layout?: string;
}
type ReturnType = BaseObj | BaseObj[];
type GeneratorReturnType = ReturnType | Promise<ReturnType>;
interface GeneratorFunction {
(locals: object, callback?: NodeJSLikeCallback<any>): GeneratorReturnType;
}
type StoreFunctionReturn = Promise<ReturnType>;
interface StoreFunction {
(locals: object): StoreFunctionReturn;
}
interface Store {
[key: string]: StoreFunction;
}
declare class Generator {
id: number;
store: Store;
constructor();
list(): Store;
get(name: string): StoreFunction;
register(fn: GeneratorFunction): void;
register(name: string, fn: GeneratorFunction): void;
}
export = Generator;

可以看到这里声明了我们使用的 register 方法,以及 GeneratorFunction 的定义。
GeneratorFunction 定义的返回值类型 BaseObj 正对应了我们上面发现的 generator 方法的返回值的三个字段:

1
2
3
4
5
interface BaseObj {
path: string;
data: any;
layout?: string;
}

回到具体的 generator

dir

我们可以看到 node_modules 中引入了相当多 hexo-generator-* 的依赖,就是这些依赖帮助我们控制如何生成对应的网页。

我们以 hexo-generator-tag 进行举例,首先在 index.js 中注册 generator

hexo-generator-tag/index.js
1
2
3
4
5
6
7
8
9
10
11
/* global hexo */
"use strict";

hexo.config.tag_generator = Object.assign(
{
per_page: hexo.config.per_page == null ? 10 : hexo.config.per_page,
},
hexo.config.tag_generator
);

hexo.extend.generator.register("tag", require("./lib/generator"));

然后在 lib 中提供 generator 的方法,并返回带有 path, layoutdata 的数据。

hexo-generator-tag/lib/generator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ......
pages.push({
path: tagDir,
layout: ["tag-index", "tag", "archive", "index"],
posts: locals.posts,
data: {
base: tagDir,
total: 1,
current: 1,
current_url: tagDir,
posts: locals.posts,
prev: 0,
prev_link: "",
next: 0,
next_link: "",
tags: tags,
},
});

return pages;
// ......

总结生成器生成 html 的流程

首先 hexo 会去 node_modules 下面遍历 hexo-generator-* 的依赖,把这些 generator 都进行加载。
然后 generator 执行,返回带有 path, layoutdata 结构的数据。
其中 path 对应了生成文件的路径,layout 指定了渲染 theme 下的哪一个模板。
最后 hexo 通过 createLoadThemeRoute 方法将 generator 生成的数据和 theme 下的模板文件结合到一起,生成出最终的 html 文件。

解答开篇的问题

通过上面的代码分析,可以得知前两个比较 common 的问题的答案。
下面我们主要分析下剩下两个具体问题的答案:

  1. 为什么我的 archives/index.html 会利用 theme/layout/archive.ejs 生成?

    解答这个问题需要查看 hexo-generator-achive 的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // ......
    function generate(path, posts, options = {}) {
    options.archive = true;

    result.push(
    ...pagination(path, posts, {
    perPage,
    layout: ["archive", "index"],
    format: paginationDir + "/%d/",
    data: options,
    })
    );
    }
    // ......

    我们可以看到 hexo-generator-achive 返回数据的 layout 指定了 archive 作为模板文件渲染。所以生成的 archives/index.html 自然利用了 theme/layout/archive.ejs 进行了代码的生成。

  2. 为什么 tags/index.html 没有生成,/tags/tag-a.html 却生成了,并且利用了 theme/tag.ejs 这个模板?

    解答这个问题需要查看 hexo-generator-tag 的代码:

    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
    //......
    // generate tag index page, usually /tags/index.html
    if (config.tag_generator.enable_index_page) {
    tagDir = config.tag_dir;
    if (tagDir[tagDir.length - 1] !== "/") {
    tagDir += "/";
    }

    pages.push({
    path: tagDir,
    layout: ["tag-index", "tag", "archive", "index"],
    posts: locals.posts,
    data: {
    base: tagDir,
    total: 1,
    current: 1,
    current_url: tagDir,
    posts: locals.posts,
    prev: 0,
    prev_link: "",
    next: 0,
    next_link: "",
    tags: tags,
    },
    });
    }
    //......

    我们可以看到,如果 config.tag_generator.enable_index_page 为空或者没有定义,就不会生成 path 的值是 /tags/index.html

    然后再查看生成 /tags/tag-a.html 的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const pages = tags.reduce((result, tag) => {
    if (!tag.length) return result;

    const posts = tag.posts.sort(orderBy);
    const data = pagination(tag.path, posts, {
    perPage: perPage,
    layout: ["tag", "archive", "index"],
    format: paginationDir + "/%d/",
    data: {
    tag: tag.name,
    },
    });

    return result.concat(data);
    }, []);

    其中 layout 数组中,第一个指定了 tag 作为模板,所以只要在我们 theme/layout/ 文件夹下面找到了 tag.ejs 就会使用该文件去渲染具体的 tag 页面。


如果这篇文章对你有帮助,那么不妨?