51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

5 分钟带你读懂 Hexo 源码设计模式

Hexo是什么? {#Hexo是什么}

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

框架特色 {#框架特色}

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

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

Hexo命令行设计 {#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入口模块设计}

同大多数框架相同,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编译模块设计 {#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


赞(2)
未经允许不得转载:工具盒子 » 5 分钟带你读懂 Hexo 源码设计模式