Hexo是什么?

官方定义是快速、简洁且高效的博客框架,实际不仅仅于此,它是一个JS语言编写的静态网站生成器,主要作用是解析Markdown语法,并配合模板引擎,快速生成静态网站。同时,还可以自定义主题,引用第三方插件,除了搭建个人博客之外,Hexo还被许许多多的项目拿来生成API文档,如阿里开源项目WeexEgg等等。

框架特色

Node.js运行环境,速度极快,扩展能力强,强大的插件系统,可配置性高,一键编译部署,适用于博客,静态个人网站,开源项目文档,最受欢迎的JS静态网站生成器。

注意:本文所有代码均为伪代码

Hexo命令行设计

在命令行模块,Hexo选择使用minimist来解析命令行参数得到一个js对象,并建立一个Hexo实例并初始化,最后通过实例对象call方法传递命令行指令。

1
2
3
4
5
var args = minimist(process.argv.slice(2))
var cmd = args._.shift()
var hexo = new Hexo()
hexo.init()
hexo.call(cmd, args)

Hexo入口模块设计

同大多数框架相同,Hexo采用构造-原型组合模式定义类,采用组合继承的方式继承Node中EventEmitter模块,更容易得通过onemit发布与订阅事件。在实例化阶段,保存所编译文件存放的路径、输出路径及其它脚本、插件、主题等所处的路径,保存环境变量,即命令行参数、版本号等基本信息。创建扩展对象,按不同的功能进行分类,作用是创建store,用于注册句柄,获取句柄,以便后续编译过程调用,在Hexo中,扩展类型包括控制台(Console)、部署器(Deployer)、过滤器(Filter)、生成器(Generator)、辅助函数(Helper)、处理器(Processor)、渲染引擎(Renderer)等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Hexo(base, args) {
EventEmitter.call(this)
this.public_dir = path.join(base, 'public');
this.source_dir = path.join(base, 'source');
...
this.extend = {
console: new extend.Console(),
generator: new extend.Generator(),
processor: new extend.Processor(),
renderer: new extend.Renderer(),
...
}
...
}
// 等同于Object.setPrototypeOf(Hexo.prototype, EventEmitter.prototype)
require('util').inherits(Hexo, EventEmitter)

换句话说,扩展对象是一个容器,一个事件注册机,接下来要做的是在Hexo初始化阶段,加载Hexo内置插件,不断扩充容器的功能,以渲染引擎为例,向extend.renderer注册渲染过程处理函数,在其它模块中就可以很方便得从hexo的上下文中去调用渲染引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hexo.prototype.init = function() {
// 加载内部插件
require('plugins/console')(this);
require('plugins/generator')(this);
require('plugins/processor')(this);
require('plugins/renderer')(this);
...
};
// plugins/renderer 注册渲染器
module.exports = function(hexo) {
var renderer = hexo.extend.renderer;
renderer.register('swig', 'html', require('./swig'));
renderer.register('ejs', 'html', require('./ejs'));
renderer.register('yml', 'json', require('./yaml'));
};
// 调用渲染器
module.exports = function(hexo) {
var renderer = hexo.extend.renderer;
return renderer.get('ejs');
};

除了加载内部插件外,Hexo还允许加载第三方插件,用npm的方式安装依赖包或者存放在目录scripts文件夹中,巧妙的是,插件内部无需引用hexo对象,可直接使用hexo变量来访问执行上下文,正是由于框架采用的是Node中vm(Virtual Machine)模块来加载js文件,相当于模板引擎实现原理中的new Functioneval来解析并执行字符串代码。

1
2
3
4
5
6
7
8
9
// 加载外部插件
Hexo.prototype.loadPlugin = function(path) {
fs.readFile(path).then(function(script) {
script = '(function(hexo){' +
script + '});';

return vm.runInThisContext(script, path)(this);
});
};

Hexo编译模块设计

预期用户命令行接口

1
$ hexo generate

首先往Hexo扩展对象Console中注册generate函数

1
2
3
4
5
6
7
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'}
]
}, require('./generate'));

generate函数用于生成目标文件夹,从Hexo的路由模块中取得所有需要生成目标文件的路径,调用fs输出文件,在此之前,首先得对源文件进行预处理,把路径写入路由。由于Hexo本身设计的特点,源文件又分为内容和主题两部分,分别存放在source和theme文件夹中,所以得调用process函数分别对它们进行预处理。

1
2
3
4
5
function generate(hexo) {
hexo.source.process();
hexo.theme.process();
routerList.forEach(path => writeFile(path))
}

Hexo抽象出一层公用模块用来管理所有处理器,命名为Box,相当于一个容器,统一管理处理器的添加删除执行监控,并分别为source和theme创建实例,Box原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Box(base) {
this.base = base;
this.processors = [];
}

Box.prototype.addProcessor = function(pattern, fn) {
this.processors.push({
pattern: pattern,
process: fn
});
};

Box.prototype.process = function(callback) {
this.processors.forEach(processor => processor.process())
};

有了Box容器,接下来要做的就是往容器中添加处理器,同样,用插件的形式往扩展对象extend中注册句柄,再注入到Box容器中。

1
2
3
4
5
6
module.exports = function(hexo) {
var processor = hexo.extend.processor;
var obj = require('./asset')(hexo);
processor.register(obj.pattern, obj.process); // pattern为文件名匹配格式
...
};

以markdwon文件的处理为例,成功匹配到文件扩展名后,调用hexo-front-matter利用正则表达式匹配来解析文件,分离顶部元数据与主题内容,类似于gray-matter,把元数据与内容以key/value的形式转换为一个js对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 处理器
module.exports = function(hexo) {
return {
pattern: /\.md/,
process: function(path) {
readFile(path, function(err, content) {
var data = require('hexo-front-matter')(content)
data.source = path;
data.raw = content;
return data
}
}
}
}
1
2
3
4
5
6
7
// markdown文件
---
title: hello
layout: home
---
# Hexo
A fast, simple & powerful blog framework

解析成 =>

1
2
3
4
5
6
7
{
title: 'hello',
layout: 'home',
_content: '# Hexo\nA fast, simple & powerful blog framework',
source: 'README.md',
raw: '---\ntitle: hello\n---\n# Hexo\nA fast, simple & powerful blog framework'
}

下一步,Hexo定义了过滤器(Filter)的概念,借鉴于Wordpress,用于在模板渲染前后修改具体的数据,也可把它看成一个钩子,例如使用marked编译markdown文件内容。

1
2
3
4
5
6
7
hexo.execFilter('before_generate', function(data) {
hexo.render.render({
text: data._content,
path: data.source,
engine: data.engine
});
};

转换后增加一条content属性,带有标签与类名的markdown html片段。

1
2
3
4
5
6
7
8
{
title: 'hello',
layout: 'home',
_content: '# Hexo\nA fast, simple & powerful blog framework',
content: '<h1 id="Hexo"><a href="#Hexo" class="headerlink" title="Hexo"></a>Hexo</h1><p>A fast, simple & powerful blog framework</p>\n',
source: 'README.md',
raw: '---\ntitle: hello\n---\n# Hexo\nA fast, simple & powerful blog framework'
}

得到页面数据后,进入模板引擎渲染阶段,Hexo本身并不带模板引擎的实现,需要借助第三方库,如ejs,并通过一个适配器,把原接口转换为需求接口,向扩展对象extend.render中注册模板解析函数。

1
2
3
hexo.extend.renderer.register('ejs', 'html', function(data, locals) {
require('ejs').render(data, locals))
});

模板引擎解析后的函数存储在hexo.theme对象中,以文件名作为key,后续渲染时只需匹配layout就能找到指定的渲染函数,注入locals变量(上面markdwon解析后的js对象+扩展对象extend.helper定义的变量、函数),生成最终文本字符串。

1
2
var view = hexo.theme.getView(data.layout);
view.render(locals)

最后通过Node fs模块把最终文本字符串输出到public目标文件夹中,大功告成。
回顾整个工作流程,可以看作
cli => hexo init => plugin load => process => filter => render => generate

扩展阅读

此外,Hexo还有许多优秀的设计模式
##数据库系统
Hexo引入了json数据库warehouse,也是作者自己开发的一个数据库驱动,API用法与Mongoose相差无几,在架构中的角色是充当一个中介者,存储临时数据,或者持久化数据存储,如博客的发表时间等,还可以作为缓存层,比对文件的修改时间,跳过无修改文件的编译过程,减少二次编译的时间。

异步方案

大量的异步回调文件操作会让代码丧失可读性,Hexo引入Promise库bluebird,内置丰富的API,很方便的处理异步的流程控制,如使用Promise.promisify(require('fs').readFile)可以把原生fs异步函数包装成一个Promise对象,另外,随着Node7.6的正式版发布,直接支持async/await语法,可以更优雅得处理异步问题。

通用日志模块

把Log划分为六个级别,’TRACE’, ‘DEBUG’, ‘INFO ‘, ‘WARN ‘,’ERROR’,’FATAL’,不同级别输出不同的格式与颜色(chalk),并提供命令行接口,如果带有–debug字段,则Log自动降级为’TRACE’级别。

参考链接:https://juejin.cn/post/6844903469669679117