发布于 

【hexo专栏】hexo的render模块的工作原理

背景

我们看到hexo的源码中有一个render模块,这个模块的作用是什么呢?我们先来看看它的文档。

用途

hexo框架中有很多类似 .ejs, .yml, .styl 这样的文件,然后在框架中或者扩展中想要把这些文件做一个转换:

1
2
3
hexo.render.render({path: 'path/to/file.swig'}).then(function(result){
// ...
});

在渲染时,无需指定特定的engine,Hexo 会自动根据扩展名猜测所要使用的渲染引擎,当然您也可以使用 engine 指定。

使用方式

然后我们在注册这个扩展的时候,我们可以用同步和异步两种模式:

异步模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
var stylus = require('stylus');

// Callback
hexo.extend.renderer.register('styl', 'css', function(data, options, callback){
stylus(data.text).set('filename', data.path).render(callback);
});

// Promise
hexo.extend.renderer.register('styl', 'css', function(data, options){
return new Promise(function(resolve, reject){
resolve('test');
});
});

同步模式

1
2
3
4
5
6
var ejs = require('ejs');

hexo.extend.renderer.register('ejs', 'html', function(data, options){
options.filename = data.path;
return ejs.render(data.text, options);
}, true);

原理

我们看到在hexo的源码位置: lib/extend/renderer.js 中,我们可以看到如下代码:

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
class Renderer {
constructor() {
this.store = {};
this.storeSync = {};
}

list(sync) {
return sync ? this.storeSync : this.store;
}

get(name, sync) {
const store = this[sync ? 'storeSync' : 'store'];

return store[getExtname(name)] || store[name];
}

isRenderable(path) {
return Boolean(this.get(path));
}

isRenderableSync(path) {
return Boolean(this.get(path, true));
}

getOutput(path) {
const renderer = this.get(path);
return renderer ? renderer.output : '';
}

register(name, output, fn, sync) {
if (!name) throw new TypeError('name is required');
if (!output) throw new TypeError('output is required');
if (typeof fn !== 'function') throw new TypeError('fn must be a function');

name = getExtname(name);
output = getExtname(output);

if (sync) {
this.storeSync[name] = fn;
this.storeSync[name].output = output;

this.store[name] = Promise.method(fn);
} else {
if (fn.length > 2) fn = Promise.promisify(fn);
this.store[name] = fn;
}

this.store[name].output = output;
this.store[name].compile = fn.compile;
}
}

相当于它内部有一个store维护了一个扩展名和对应的渲染函数的映射关系,然后我们在注册的时候,就是往这个store中添加一个映射关系。

然后当我们调用hexo.render.render的时候,我们看到hexo对象上面的render是一个Render类对象:

1
2
3
4
5
6
7
class Hexo extends EventEmitter{
constructor(....){
...
this.render = new Render(this);
....
}
}

然后我们再看一下这个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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
class Render {
constructor(ctx) {
this.context = ctx;
this.renderer = ctx.extend.renderer;
}

isRenderable(path) {
return this.renderer.isRenderable(path);
}

isRenderableSync(path) {
return this.renderer.isRenderableSync(path);
}

getOutput(path) {
return this.renderer.getOutput(path);
}

getRenderer(ext, sync) {
return this.renderer.get(ext, sync);
}

getRendererSync(ext) {
return this.getRenderer(ext, true);
}

render(data, options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = {};
}

const ctx = this.context;
let ext = '';

let promise;

if (!data) return Promise.reject(new TypeError('No input file or string!'));

if (data.text != null) {
promise = Promise.resolve(data.text);
} else if (!data.path) {
return Promise.reject(new TypeError('No input file or string!'));
} else {
promise = readFile(data.path);
}

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 => {
result = toString(result, data);
if (data.onRenderEnd) {
return data.onRenderEnd(result);
}

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

renderSync(data, options = {}) {
if (!data) throw new TypeError('No input file or string!');

const ctx = this.context;

if (data.text == null) {
if (!data.path) throw new TypeError('No input file or string!');
data.text = readFileSync(data.path);
}

if (data.text == null) throw new TypeError('No input file or string!');

const ext = data.engine || getExtname(data.path);
let result;

if (ext && this.isRenderableSync(ext)) {
const renderer = this.getRendererSync(ext);
result = Reflect.apply(renderer, ctx, [data, options]);
} else {
result = data.text;
}

const output = this.getOutput(ext) || ext;
result = toString(result, data);

if (data.onRenderEnd) {
result = data.onRenderEnd(result);
}

return ctx.execFilterSync(`after_render:${output}`, result, {
context: ctx,
args: [data]
});
}
}

这个就比较好理解,大概就是根据类型,获取对应的渲染函数,然后调用渲染函数,最后返回渲染结果。

总结

这样hexo的扩展性就相对比较好了,比如前端有什么sass、less,然后比如变成css后,又想要Compress这个文件内容。另外我们也知道hexo的配置文件是yaml格式,那内部相当于是hexo.render.render({path: '${path.yml}'})比如转成json,然后做配置文件的合并什么的了。 最终实际的渲染,变成了对应的npm包的逻辑了,我们主要把文件的类型和渲染引擎的对应关系在Map里面建立好就好。

Hexo专栏

目前 Hexo 系列形成了一个小专栏,欢迎读者继续阅读: Hexo专栏地址


如果你有什么意见和建议,可以点击: 反馈地址 进行反馈。