上篇文章分析了hexo g过程中的文件预渲染过程。本篇文章将会分析一下文件渲染的过程。

generate 入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

还是开头的文件,核心_generate过程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_generate(options = {}) {
// ...
// before_generate里有 post文章 render,把 markdown后缀文件渲染
// 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;
});
}

这里execFilter是一个很巧妙的设计,是一个拦截器的概念,通过传入对应的拦截器名称,然后就会执行对应的逻辑。里面和其他类似,同样是通过register来注册事件。找到对应的路径。

before_generate

1
2
3
4
5
6
// hexo/lib/plugins/filter/before_generate/index.js
module.exports = (ctx) => {
const { filter } = ctx.extend

filter.register("before_generate", require("./render_post"))
}

然后看具体的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hexo/lib/plugins/filter/before_generate/render_post.js
function renderPostFilter(data) {
const renderPosts = (model) => {
// 这里只会还没有渲染过的 post文章,已经渲染过的就不会再次 render了
const posts = model.toArray().filter((post) => post.content == null)

return Promise.map(posts, (post) => {
post.content = post._content
post.site = { data }

// full_source 是目标文件路径
return this.post.render(post.full_source, post).then(() => post.save())
})
}

return Promise.all([
renderPosts(this.model("Post")),
renderPosts(this.model("Page")),
])
}

先过滤了post.content已经存在的文章,然后调用this.post.render来做最后的渲染,即把 json 结构数据->最终的 html 结构。

post.render

来看后面的 render 过程。

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
// hexo/lib/hexo/post.js
render(source, data = {}, callback) {
// ...
return promise.then(content => {
data.content = content;
// Run "before_post_render" filters
// 里面做的一个操作就是把 ```代码块``` 渲染成 html标签
return ctx.execFilter('before_post_render', data, { context: ctx });
}).then(() => {
// ...
ctx.log.debug('Rendering post: %s', magenta(source));
// hexo-renderer-marked 插件注册的渲染逻辑
// Render with markdown or other renderer
return ctx.render.render({
text: data.content,
path: source,
engine: data.engine,
toString: true,
onRenderEnd(content) {
// Replace cache data with real contents
data.content = cacheObj.restoreAllSwigTags(content);

// Return content after replace the placeholders
if (disableNunjucks) return data.content;

// Render with Nunjucks
return tag.render(data.content, data);
}
}, options);
}).then(content => {
data.content = cacheObj.restoreCodeBlocks(content);

// Run "after_post_render" filters
return ctx.execFilter('after_post_render', data, { context: ctx });
}).asCallback(callback);
}

上面的代码分为三步骤:before_post_renderFilter 逻辑、render 逻辑、after_post_renderFilter 逻辑。后面分别介绍。

before_post_render

before_post_render做的一个很关键的步骤就是代码块渲染,一般代码块会以```形式存在,而这里做的逻辑就是把代码块转换成 html 标签结构。

1
2
3
4
5
### Create a new post

\`\`\` bash
$ hexo new "My New Post"
\`\`\`

例如上面的结构会被转换成下面的结构。

1
2
3
### Create a new post

<hexoPostRenderCodeBlock><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure></hexoPostRenderCodeBlock>

render 过程

render 过程同样是使用了最开始的 extends 扩展中注册的方法。我们来看一下具体的ctx.render实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hexo/lib/hexo/render.js
render(data, options, callback) {
// ...
return promise.then(text => {
data.text = text;
ext = data.engine || getExtname(data.path);
if (!ext || !this.isRenderable(ext)) return text;

const renderer = this.getRenderer(ext);
return Reflect.apply(renderer, ctx, [data, options]);
}).then(result => {
// ...
}).then(result => {
const output = this.getOutput(ext) || ext;
return ctx.execFilter(`after_render:${output}`, result, {
context: ctx,
args: [data]
});
}).asCallback(callback);
}

我们依然是只看一下核心实现。现实根据后缀拿到对应的渲染方法,这里的渲染方法和其他的结构类似,都是注册上去的。

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

// ...
const yaml = require("./yaml")

renderer.register("yml", "json", yaml, true)
renderer.register("yaml", "json", yaml, true)

const nunjucks = require("./nunjucks")

renderer.register("njk", "html", nunjucks, true)
renderer.register("j2", "html", nunjucks, true)
}

上述代码就是对应的注册逻辑,即把 yml 后缀渲染成 json 格式的其中一个方法。那么如果我们想扩展一些渲染方法,那么可以在插件中去做这件事情。比如hexo-renderer-ejshexo-renderer-infernohexo-renderer-marked都是这样做的。你可以去对应的插件代码上查看。md 后缀的文件就是通过hexo-renderer-marked这个插件做的渲染过程,最终会生成 html 标签结构的文件。

after_post_render

里面做了一些收尾操作,比如excerpt.js做的事情就是匹配到 md 文章中的<!--more-->这个标示,然后渲染成<span id="more"></span>这个结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const rExcerpt = /<!-- ?more ?-->/i

function excerptFilter(data) {
const { content } = data

if (typeof data.excerpt !== "undefined") {
data.more = content
} else if (rExcerpt.test(content)) {
data.content = content.replace(rExcerpt, (match, index) => {
data.excerpt = content.substring(0, index).trim()
data.more = content.substring(index + match.length).trim()

return '<span id="more"></span>'
})
} else {
data.excerpt = ""
data.more = content
}
}

执行到这一步,文章的 render 过程基本已经完成。后续的部分将是 generators 的关键过程。

_runGenerators

这一步骤会调用之前注册的所有 generator,包含插件注册的,比如hexo-generator-archivehexo-generator-category等。

1
2
3
4
07:59:58.367 DEBUG Generator: page
07:59:58.367 DEBUG Generator: post
07:59:58.367 DEBUG Generator: archive
// ...

赋值 layout

还是以 post 文章为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hexo/lib/plugins/generator/post.js
function postGenerator(locals) {
// ...
return posts.map((post, i) => {
// ...
const layouts = ["post", "page", "index"]
if (layout !== "post") layouts.unshift(layout)
post.__post = true

return {
path,
layout: layouts,
data: post,
}
})
}

对于 post 文章来讲,梳理了一下数据结构。会转换成类似下面的结构。data 里面的内容还是之前处理出来的数据,多出来的比较关键的数据就是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
{
path: '2021/09/30/hello-world/',
layout: [ 'post', 'page', 'index' ],
data: {
title: 'Hello World',
_content: 'Welcome to [Hexo](https://hexo.io/)',
source: '_posts/hello-world.md',
raw: '---\n' +
'title: Hello World\n' +
'---\n' +
'Welcome to [Hexo](https://hexo.io/)',
slug: 'hello-world',
published: true,
date: Moment<2021-09-30T15:24:18+08:00>,
updated: Moment<2021-09-30T15:24:18+08:00>,
comments: true,
layout: 'post',
photos: [],
link: '',
_id: 'ckuzae01t0000jpp2464lgr8v',
content: '<p>Welcome to <a href="https://hexo.io/">Hexo</a>',
site: { data: {} },
excerpt: '',
more: '<p>Welcome to <a href="https://hexo.io/">Hexo</a>',
path: [Getter],
permalink: [Getter],
full_source: [Getter],
asset_dir: [Getter],
tags: [Getter],
categories: [Getter],
__post: true
}
}

然后接下来继续看下面的逻辑。把数据最终转换成对应的文件结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// hexo/lib/hexo/index.js
_routerReflesh(runningGenerators, useCache) {
// ...
return runningGenerators.map(generatorResult => {
// ...
// add Route
const path = route.format(generatorResult.path);
const { data, layout } = generatorResult;
// ...
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
// ...
});
}

根据 layout 逐个生成

里面比较关键的逻辑就是这个createLoadThemeRoute,里面调用了view.render

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}: ${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: ${magenta(path)}`)
})
}
}
}
}

比如layout: ['post', 'idnex', 'archive']这种情况,会分别使用对应的view.render,而view.render是在 theme 预处理的阶段做的,里面会遍历 theme 文件,以 icarus 为例,里面的layout/post.jsx路径其实就是代表了这种post的渲染方式,我们将 view 打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// post
view: _View {
path: 'post.jsx',
source: '/Users/huidizhu/Personal/blog/blog-test/node_modules/hexo-theme-icarus/layout/post.jsx',
data: {
_content: "const { Component } = require('inferno');\n" +
"const Article = require('./common/article');\n" +
'\n' +
'module.exports = class extends Component {\n' +
' render() {\n' +
' const { config, page, helper } = this.props;\n' +
'\n' +
' return <Article config={config} page={page} helper={helper} index={false} />;\n' +
' }\n' +
'};\n'
},
_compiledSync: [Function (anonymous)],
_compiled: [Function (anonymous)]
}

这里的 _compiled就是对应 post 生成的关键方法。继续去找 _compiled的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hexo/lib/theme/view.js _precompile
const renderer = render.getRenderer(ext)

if (renderer && typeof renderer.compile === "function") {
const compiled = renderer.compile(data)

this._compiledSync = (locals) => {
const result = compiled(locals)
return ctx.execFilterSync(...buildFilterArguments(result))
}

this._compiled = (locals) =>
Promise.resolve(compiled(locals)).then((result) =>
ctx.execFilter(...buildFilterArguments(result))
)
}

里面其实就是拿到了对应的 jsx渲染方法,即在hexo-renderer-inferno插件中定义的 jsx 渲染方法。