5 分钟带你读懂 Hexo 源码设计模式
Hexo是什么?
官方定义是快速、简洁且高效的博客框架,实际不仅仅于此,它是一个JS语言编写的静态网站生成器,主要作用是解析Markdown语法,并配合模板引擎,快速生成静态网站。同时,还可以自定义主题,引用第三方插件,除了搭建个人博客之外,Hexo还被许许多多的项目拿来生成API文档,如阿里开源项目Weex、Egg等等。
框架特色
Node.js运行环境,速度极快,扩展能力强,强大的插件系统,可配置性高,一键编译部署,适用于博客,静态个人网站,开源项目文档,最受欢迎的JS静态网站生成器。
注意:本文所有代码均为伪代码
Hexo命令行设计
在命令行模块,Hexo选择使用minimist来解析命令行参数得到一个js对象,并建立一个Hexo实例并初始化,最后通过实例对象call方法传递命令行指令。
1 | var args = minimist(process.argv.slice(2)) |
Hexo入口模块设计
同大多数框架相同,Hexo采用构造-原型组合模式定义类,采用组合继承的方式继承Node中EventEmitter模块,更容易得通过on
与emit
发布与订阅事件。在实例化阶段,保存所编译文件存放的路径、输出路径及其它脚本、插件、主题等所处的路径,保存环境变量,即命令行参数、版本号等基本信息。创建扩展对象,按不同的功能进行分类,作用是创建store,用于注册句柄,获取句柄,以便后续编译过程调用,在Hexo中,扩展类型包括控制台(Console)、部署器(Deployer)、过滤器(Filter)、生成器(Generator)、辅助函数(Helper)、处理器(Processor)、渲染引擎(Renderer)等等。
1 | function Hexo(base, args) { |
换句话说,扩展对象是一个容器,一个事件注册机,接下来要做的是在Hexo初始化阶段,加载Hexo内置插件,不断扩充容器的功能,以渲染引擎为例,向extend.renderer注册渲染过程处理函数,在其它模块中就可以很方便得从hexo的上下文中去调用渲染引擎。
1 | Hexo.prototype.init = function() { |
除了加载内部插件外,Hexo还允许加载第三方插件,用npm的方式安装依赖包或者存放在目录scripts文件夹中,巧妙的是,插件内部无需引用hexo对象,可直接使用hexo变量来访问执行上下文,正是由于框架采用的是Node中vm
(Virtual Machine)模块来加载js文件,相当于模板引擎实现原理中的new Function
或eval
来解析并执行字符串代码。
1 | // 加载外部插件 |
Hexo编译模块设计
预期用户命令行接口
1 | $ hexo generate |
首先往Hexo扩展对象Console中注册generate
函数
1 | console.register('generate', 'Generate static files.', { |
generate
函数用于生成目标文件夹,从Hexo的路由模块中取得所有需要生成目标文件的路径,调用fs
输出文件,在此之前,首先得对源文件进行预处理,把路径写入路由。由于Hexo本身设计的特点,源文件又分为内容和主题两部分,分别存放在source和theme文件夹中,所以得调用process
函数分别对它们进行预处理。
1 | function generate(hexo) { |
Hexo抽象出一层公用模块用来管理所有处理器,命名为Box,相当于一个容器,统一管理处理器的添加删除执行监控,并分别为source和theme创建实例,Box原型如下:
1 | function Box(base) { |
有了Box容器,接下来要做的就是往容器中添加处理器,同样,用插件的形式往扩展对象extend中注册句柄,再注入到Box容器中。
1 | module.exports = function(hexo) { |
以markdwon文件的处理为例,成功匹配到文件扩展名后,调用hexo-front-matter利用正则表达式匹配来解析文件,分离顶部元数据与主题内容,类似于gray-matter,把元数据与内容以key/value的形式转换为一个js对象。
1 | // 处理器 |
1 | // markdown文件 |
解析成 =>
1 | { |
下一步,Hexo定义了过滤器(Filter)的概念,借鉴于Wordpress,用于在模板渲染前后修改具体的数据,也可把它看成一个钩子,例如使用marked编译markdown文件内容。
1 | hexo.execFilter('before_generate', function(data) { |
转换后增加一条content属性,带有标签与类名的markdown html片段。
1 | { |
得到页面数据后,进入模板引擎渲染阶段,Hexo本身并不带模板引擎的实现,需要借助第三方库,如ejs,并通过一个适配器,把原接口转换为需求接口,向扩展对象extend.render中注册模板解析函数。
1 | hexo.extend.renderer.register('ejs', 'html', function(data, locals) { |
模板引擎解析后的函数存储在hexo.theme对象中,以文件名作为key,后续渲染时只需匹配layout就能找到指定的渲染函数,注入locals变量(上面markdwon解析后的js对象+扩展对象extend.helper定义的变量、函数),生成最终文本字符串。
1 | var view = hexo.theme.getView(data.layout); |
最后通过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