hexo源码分析

hexo默认的分类页面,每个分类都需要跳转才能到子页面,这样不够一目了然,考虑到当前只有百来篇博文,没必要每个分类单独跳转页面,因此打算自己实现这个功能

Hexo添加自定义分类菜单项并定制页面布局(简洁版)大致可以知道实现一个模板文件

然后注入js代码例如

1
2
3
4
5
6
hexo.extend.generator.register("test", function (locals) {
return {
path: "test/index.html",
data: "foo",
layout: ["test"],
};

就可以达到目的

但是hexo的文档写的非常的烂,关于generator的文档描述了一些常见用法,但是没有传递给模板文件的方式

所以只能从源码入手了

hexo命令入口

我对nodejs不太熟悉,所以首先分析了一下hexo命令的调用流程,查看hexo命令在哪里

1
2
$ which hexo
/c/Users/tedcy/AppData/Roaming/npm/hexo

而环境变量$PATH中存在/c/Users/tedcy/AppData/Roaming/npm这个路径,因此应该是npm install的时候c从在博客根目录下node_modules/hexo/node_modules/hexo-cli/自动安装进npm目录的

hexo-cli模块

hexo命令的内容指向了node_modules/hexo/node_modules/hexo-cli/lib/hexo.js

1
2
3
4
5
#!/usr/bin/env node

'use strict';

require('../lib/hexo')();

hexo.js这个文件的entry函数是实际入口,通过命令获取到事先注册在hexo.extend.console上的回调,然后通过hexo.call来调用回调:

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
45
46
47
48
49
50
51
//获取到核心hexo类并进行创建
function loadModule(path, args) {
return Promise.try(() => {
const modulePath = resolve.sync('hexo', { basedir: path });
const Hexo = require(modulePath);

return new Hexo(path, args);
});
}

function entry(cwd = process.cwd(), args) {
//...
return findPkg(cwd, args).then(path => {
if (!path) return;

//这里找到核心的Hexo类路径在
//node_modules/hexo/lib/hexo/index.js
hexo.base_dir = path;

return loadModule(path, args).catch(err => {
log.error(err.message);
log.error('Local hexo loading failed in %s', chalk.magenta(tildify(path)));
log.error('Try running: \'rm -rf node_modules && npm install --force\'');
throw new HexoNotFoundError();
});
}).then(mod => { //这里的mod就是创建好的Hexo类
if (mod) hexo = mod;
log = hexo.log;

require('./console')(hexo);

return hexo.init(); //初始化Hexo类
}).then(() => {
let cmd = 'help';

if (!args.h && !args.help) {
const c = args._.shift();
if (c && hexo.extend.console.get(c)) cmd = c; //获取注册的回调
}

watchSignal(hexo);

return
hexo.call(cmd, args).then(() => hexo.exit()).catch(err => hexo.exit(err).then(() => {
// `hexo.exit()` already dumped `err`
handleError(null);
}));
}).catch(handleError);
}

module.exports = entry;

那么是在哪里注册回调的呢,在项目目录下执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cd node_modules && grep -nr console.register node_modules
./hexo/node_modules/hexo-cli/lib/console/index.js:6: console.register('help', 'Get help on a command.', {}, require('./help'));
./hexo/node_modules/hexo-cli/lib/console/index.js:8: console.register('init', 'Create a new Hexo folder.', {
./hexo/node_modules/hexo-cli/lib/console/index.js:20: console.register('version', 'Display version information.', {}, require('./version'));
./hexo/lib/plugins/console/index.js:6: console.register('clean', 'Remove generated files and cache.', require('./clean'));
./hexo/lib/plugins/console/index.js:8: console.register('config', 'Get or set configurations.', {
./hexo/lib/plugins/console/index.js:16: console.register('deploy', 'Deploy your website.', {
./hexo/lib/plugins/console/index.js:23: console.register('generate', 'Generate static files.', {
./hexo/lib/plugins/console/index.js:33: console.register('list', 'List the information of the site', {
./hexo/lib/plugins/console/index.js:41: console.register('migrate', 'Migrate your site from other system to Hexo.', {
./hexo/lib/plugins/console/index.js:49: console.register('new', 'Create a new post.', {
./hexo/lib/plugins/console/index.js:62: console.register('publish', 'Moves a draft post from _drafts to _posts folder.', {
./hexo/lib/plugins/console/index.js:70: console.register('render', 'Render files with renderer plugins.', {
./hexo-server/index.js:14:hexo.extend.console.register('server', 'Start the server.', {

hero-server模块

可以看到hexo server是在node_modules/hexo-server/index.js下注册的,它需要调用node_modules/hexo-server/lib/server.js

1
2
3
4
5
6
7
8
9
10
hexo.extend.console.register('server', 'Start the server.', {
desc: 'Start the server and watch for file changes.',
options: [
{name: '-i, --ip', desc: 'Override the default server IP. Bind to all IP address by default.'},
{name: '-p, --port', desc: 'Override the default port.'},
{name: '-s, --static', desc: 'Only serve static files.'},
{name: '-l, --log [format]', desc: 'Enable logger. Override log format.'},
{name: '-o, --open', desc: 'Immediately open the server url in your default web browser.'}
]
}, require('./lib/server'));

接着查看node_modules/hexo-server/lib/server.js

1
2
3
4
5
6
7
8
module.exports = function(args) {
//...
if (args.s || args.static) {
return this.load();
}

return this.watch();
}

这里的this类就是之前在node_modules/hexo/node_modules/hexo-cli/lib/hexo.js中通过loadModule动态创建好的Hexo类

核心Hexo类

源码位于node_modules/hexo/lib/hexo/index.js

续接上面的this.load(),这部分实现会调用_generate(),从而进入生成页面的核心流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Hexo extends EventEmitter {
load(callback) {
return loadDatabase(this).then(() => {
this.log.info('Start processing');

return Promise.all([
this.source.process(),
this.theme.process()
]);
}).then(() => {
mergeCtxThemeConfig(this);
return this._generate({ cache: false });
}).asCallback(callback);
}
}
module.exports = Hexo;

_generate()中将_runGenerators()得到的返回值传递给了_routerReflesh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_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', this.locals.get('data'), { context: this })
.then(() => this._routerReflesh(this._runGenerators(), useCache)).then(() => {
this.emit('generateAfter');

// Run after_generate filters
return this.execFilter('after_generate', null, { context: this });
}).finally(() => {
this._isGenerating = false;
});
}

generate生成

那么首先查看_runGenerators()返回了什么,这里获取到this.extend.generator.list()的全部回调,对其执行,然后结果对其key和返回值进行map,reduce拼在一个result中

这里list到的全部generator显然就是通过hexo.extend.generator.register注册进来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_runGenerators() {
this.locals.invalidate();
const siteLocals = this.locals.toObject();
const generators = this.extend.generator.list();
const { log } = this;

// Run generators
return Promise.map(Object.keys(generators), key => {
const generator = generators[key];

log.debug('Generator: %s', magenta(key));
return Reflect.apply(generator, this, [siteLocals]);
//使用以下语句可以打印每个generator的返回值
//return Reflect.apply(generator, this, [siteLocals]).then(data => {
// log.info('Generator %s returned data: %o', key, data)
// return data;
//})
}).reduce((result, data) => {
return data ? result.concat(data) : result;
}, []);
}

对于如下的生成器代码

1
2
3
4
5
6
hexo.extend.generator.register("test", function (locals) {
return {
path: "test/index.html",
data: "foo",
layout: ["test"],
};

使用注释中的代码打印将得到

1
2
3
4
5
Generator test returned data: {
path: 'test/index.html',
data: 'foo',
layout: ['test', [length]: 1 ]
}

根据layout路由generate生成结果

那么传递给的_routerReflesh()大致可以猜测出来,是根据generator的key和data中的值进行分发

根据data中的layout字段转发给对应模板,为了传递更多信息,会通过Locals类对data再包一层

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
_generateLocals() {
const { config, env, theme, theme_dir } = this;
const ctx = { config: { url: this.config.url } };
const localsObj = this.locals.toObject();

class Locals {
constructor(path, locals) {
this.page = { ...locals };
if (this.page.path == null) this.page.path = path;
this.path = path;
this.url = full_url_for.call(ctx, path);
this.config = config;
this.theme = theme.config;
this.layout = 'layout';
this.env = env;
this.view_dir = join(theme_dir, 'layout') + sep;
this.site = localsObj;
}
}

return Locals;
}

_routerReflesh(runningGenerators, useCache) {
const { route } = this;
const routeList = route.list();
const Locals = this._generateLocals(); //该函数返回了一个Locals类
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);
const { data, layout } = generatorResult;

if (!layout) {
//不存在layout的时候直接返回data了
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);
}
}
});
}

简化上述代码,传递给createLoadThemeRoute()的参数locals是对data的封装

1
2
3
4
5
6
7
const Locals = this._generateLocals(); //该函数返回了一个Locals类
locals = new Locals(path, data); //根据Locals的构造函数,data传递给了this.page
// class Locals {
// constructor(path, locals) {
// this.page = { ...locals };
// }
route.set(path, createLoadThemeRoute(generatorResult, locals, this);

createLoadThemeRoute的逻辑我没太看明白,但是可以大致猜测,主要逻辑是:

当存在缓存的时候,不进行重新生成,然后遍历全部的layout,找到存在的layout,将其转发给模板

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
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}: ${magenta(path)}`);
//log.debug(`Rendering HTML ${name}: ${magenta(path)}`, locals.page);
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: ${magenta(path)}`);
});
}
}

log.warn(`No layout: ${magenta(path)}`);
};
};

这里的view.render(locals)

对于nunjucks模板而言,在node_modules/hexo/lib/plugins/renderer/nunjucks.js中存在代码是大致可以对应上的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function njkCompile(data) {
let env;
if (data.path) {
env = nunjucks.configure(dirname(data.path), nunjucksCfg);
} else {
env = nunjucks.configure(nunjucksCfg);
}
nunjucksAddFilter(env);

const text = 'text' in data ? data.text : readFileSync(data.path);

return nunjucks.compile(text, env, data.path);
}

function njkRenderer(data, locals) {
return njkCompile(data).render(locals);
}

从而完成了generator返回值到模板传参到分析