上篇文章分析了配置加载、插件加载两个事情,本篇文章将继续分析后面的流程。还是会以hexo g为例,将会分析一下文件预处理的过程。

hexo g 入口

这一部分在hexo源码分析(一)中已经分析了实现方式。先来看hexo g的入口实现。

1
2
3
4
5
6
7
8
9
10
// hexo/lib/plugins/console/index.js
console.register('generate', 'Generate static files.', {
options: [
{name: '-d, --deploy', desc: 'Deploy after generated'},
{name: '-f, --force', desc: 'Force regenerate'},
{name: '-w, --watch', desc: 'Watch file changes'},
{name: '-b, --bail', desc: 'Raise an error if any unhandled exception is thrown during generation'},
{name: '-c, --concurrency', desc: 'Maximum number of files to be generated in parallel. Default is infinity'}
]
}, require('./generate'));

执行hexo generate之后会去找generate的实现。

1
2
3
4
5
6
7
8
9
10
// hexo/lib/plugins/console/generate.js
function generateConsole(args = {}) {
// ...
// firstGenerate只是从 Router中将对应文件生成
return this.load().then(() => generator.firstGenerate()).then(() => {
if (generator.deploy) {
return generator.execDeploy();
}
});
}

这里调用了ctx.load,我们再去看load的实现。

文件预渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// hexo g 的过程
load(callback) {
return loadDatabase(this).then(() => {
this.log.info('Start processing');

// 这里应该只是把对应的 source下文件和 theme(node_modules/hexo-theme-*)的文件放入 Cache中(db.json)
return Promise.all([
this.source.process(),
this.theme.process()
]);
}).then(() => {
mergeCtxThemeConfig(this);
return this._generate({cache: false});
}).asCallback(callback);
}

先是加载了db.json,因为每次生成文件都没有必要全部渲染生成,因此这里db.json就是存储记录了当前的整个静态页面的情况,对于一些不需要重复渲染的,就会直接跳过。其中this.source.process()this.theme.process()是文件预处理。我们来看看对应的实现。

1
2
3
4
5
6
7
class Source extends Box {
constructor(ctx) {
super(ctx, ctx.source_dir);

this.processors = ctx.extend.processor.list();
}
}

以Source类为例,里面继承了Box,Theme同样,我们再来看Box实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
process(callback) {
const { base, Cache, context: ctx } = this;

// stat作用,判断目标文件是否存在
return stat(base).then(stats => {
if (!stats.isDirectory()) return;

// Check existing files in cache
const relativeBase = escapeBackslash(base.substring(ctx.base_dir.length));
// 这里其实就是找 db.json中的 Cache字段
const cacheFiles = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length));

// Handle deleted files
return this._readDir(base)
.then(files => cacheFiles.filter(path => !files.includes(path)))
.map(path => this._processFile(File.TYPE_DELETE, path));
}).catch(err => {
if (err && err.code !== 'ENOENT') throw err;
}).asCallback(callback);
}

里面先是去找了db.json中的Cache字段,Cache字段就是为了确认文件是否修改过,可以根据hash来做判断,针对没有修改过的文件,可以不做处理,仅仅对有改动的文件做处理,我们可以看一下Cache字段长什么样。

1
2
3
4
5
6
7
8
"Cache": [
{
"_id": "source/_posts/1. hexo博客构建.md",
"hash": "cea7c5c70883098af40c8785fbd9674bbdec5b6d",
"modified": 1632984281348
},
// ...
]

然后会调用_readDir去递归查找目录下面的所有文件,并通过results依赖传递,将所有的路径返回。

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
_readDir(base, prefix = '') {
const results = [];
return readDirWalker(base, results, this.ignore, prefix)
.return(results)
.map(path => this._checkFileStatus(path))
.map(file => this._processFile(file.type, file.path).return(file.path));
}

// 读取目标目录下的所有文件,并通过 result依赖传递,存入所有文件路径
function readDirWalker(base, results, ignore, prefix) {
if (isIgnoreMatch(base, ignore)) return Promise.resolve();

return Promise.map(readdir(base).catch(err => {
if (err && err.code === 'ENOENT') return [];
throw err;
}), async path => {
const fullpath = join(base, path);
const stats = await stat(fullpath);
const prefixdPath = `${prefix}${path}`;
if (stats.isDirectory()) {
return readDirWalker(fullpath, results, ignore, `${prefixdPath}/`);
}
if (!isIgnoreMatch(fullpath, ignore)) {
results.push(prefixdPath);
}
});
}

然后来看下是如何处理文件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_processFile(type, path) {
// ...
return Promise.reduce(this.processors, (count, processor) => {
const params = processor.pattern.match(path);
if (!params) return count;

const file = new File({
source: join(base, path),
path,
params,
type
});

return Reflect.apply(Promise.method(processor.process), ctx, [file])
.thenReturn(count + 1);
}, 0).then(count => {
if (count) {
ctx.log.debug('Processed: %s', magenta(path));
}
})
}

里面非常关键的就是这个this.processors,定义了预处理的逻辑。以Source为例,里面source/_post一般是存放md后缀的博客文章的。而 processor的注册是在上文分析的 extends中。

processor扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
// hexo/lib/plugins/processor/index.js
module.exports = ctx => {
const { processor } = ctx.extend;

function register(name) {
const obj = require(`./${name}`)(ctx);
processor.register(obj.pattern, obj.process);
}

register('asset');
register('data');
register('post');
};

hexo-front-matter 分离 md文件

看一下里面post的大概实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
// hexo/lib/plugins/processor/post.js
const { parse: yfm } = require('hexo-front-matter');

function processPost(ctx, file) {
// ...
return Promise.all([
file.stat(),
file.read()
]).spread((stats, content) => {
//
const data = yfm(content);
});
}

只截取了很小一部分,里面一个很关键的步骤就是使用hexo-front-matter来处理文章内容。

1
2
3
4
5
6
---
title: Hello World
---

Welcome to [Hexo](https://hexo.io/)! This is your very first post. Check [documentation](https://hexo.io/docs/) for more info. If you get any problems when using Hexo, you can find the answer in [troubleshooting](https://hexo.io/docs/troubleshooting.html) or you can ask me on [GitHub](https://github.com/hexojs/hexo/issues).
// ...

举个很简单的例子,以hexo默认的第一篇文章为例,里面的结构结构是上面的,会用---来分割 yml格式的数据和下面的文章内容。hexo-front-matter就是为了解析这个内容,最终会解析成下面的结构。

1
2
3
4
{
title: 'Hello World',
_content: 'Welcome to [Hexo](https://hexo.io/)! This is your very first post. Check [documentation](https://hexo.io/docs/) for more info. If you get any problems when using Hexo, you can find the answer in [troubleshooting](https://hexo.io/docs/troubleshooting.html) or you can ask me on [GitHub](https://github.com/hexojs/hexo/issues).\n',
}

预处理的逻辑就是诸如此类,把一些需要预先处理的文件,先做第一层处理,方便后面render过程。

小结

这里简单梳理了一下文件预处理的过程,本想继续梳理一下render过程,篇幅有限,下篇文章再进行梳理。