Hexo是什么?
官方定義是快速、簡潔且高效的博客框架,實際不僅僅于此奄容,它是一個JS語言編寫的靜態(tài)網(wǎng)站生成器,主要作用是解析Markdown語法产徊,并配合模板引擎昂勒,快速生成靜態(tài)網(wǎng)站。同時舟铜,還可以自定義主題戈盈,引用第三方插件,除了搭建個人博客之外,Hexo還被許許多多的項目拿來生成API文檔塘娶,如著名開源項目VueJS归斤、Weex、Egg等等刁岸。
框架特色
Node.js運行環(huán)境脏里,速度極快,擴展能力強虹曙,強大的插件系統(tǒng)迫横,可配置性高,一鍵編譯部署酝碳,適用于博客矾踱,靜態(tài)個人網(wǎng)站,開源項目文檔疏哗,最受歡迎的JS靜態(tài)網(wǎng)站生成器呛讲。
注意:本文所有代碼均為偽代碼
Hexo命令行設(shè)計
在命令行模塊,Hexo選擇使用minimist來解析命令行參數(shù)得到一個js對象返奉,并建立一個Hexo實例并初始化贝搁,最后通過實例對象call方法傳遞命令行指令。
var args = minimist(process.argv.slice(2))
var cmd = args._.shift()
var hexo = new Hexo()
hexo.init()
hexo.call(cmd, args)
Hexo入口模塊設(shè)計
同大多數(shù)框架相同衡瓶,Hexo采用構(gòu)造-原型組合模式定義類徘公,采用組合繼承的方式繼承Node中EventEmitter模塊牲证,更容易得通過on
與emit
發(fā)布與訂閱事件哮针。在實例化階段,保存所編譯文件存放的路徑坦袍、輸出路徑及其它腳本十厢、插件、主題等所處的路徑捂齐,保存環(huán)境變量蛮放,即命令行參數(shù)、版本號等基本信息奠宜。創(chuàng)建擴展對象包颁,按不同的功能進行分類,作用是創(chuàng)建store压真,用于注冊句柄娩嚼,獲取句柄,以便后續(xù)編譯過程調(diào)用滴肿,在Hexo中岳悟,擴展類型包括控制臺(Console)、部署器(Deployer)、過濾器(Filter)贵少、生成器(Generator)呵俏、輔助函數(shù)(Helper)、處理器(Processor)滔灶、渲染引擎(Renderer)等等普碎。
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內(nèi)置插件萄涯,不斷擴充容器的功能绪氛,以渲染引擎為例,向extend.renderer注冊渲染過程處理函數(shù)涝影,在其它模塊中就可以很方便得從hexo的上下文中去調(diào)用渲染引擎枣察。
Hexo.prototype.init = function() {
// 加載內(nèi)部插件
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'));
};
// 調(diào)用渲染器
module.exports = function(hexo) {
var renderer = hexo.extend.renderer;
return renderer.get('ejs');
};
除了加載內(nèi)部插件外,Hexo還允許加載第三方插件燃逻,用npm的方式安裝依賴包或者存放在目錄scripts文件夾中序目,巧妙的是,插件內(nèi)部無需引用hexo對象伯襟,可直接使用hexo變量來訪問執(zhí)行上下文猿涨,正是由于框架采用的是Node中vm
(Virtual Machine)模塊來加載js文件,相當(dāng)于模板引擎實現(xiàn)原理中的new Function
或eval
來解析并執(zhí)行字符串代碼姆怪。
// 加載外部插件
Hexo.prototype.loadPlugin = function(path) {
fs.readFile(path).then(function(script) {
script = '(function(hexo){' +
script + '});';
return vm.runInThisContext(script, path)(this);
});
};
Hexo編譯模塊設(shè)計
預(yù)期用戶命令行接口
$ hexo generate
首先往Hexo擴展對象Console中注冊generate
函數(shù)
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
函數(shù)用于生成目標(biāo)文件夾叛赚,從Hexo的路由模塊中取得所有需要生成目標(biāo)文件的路徑,調(diào)用fs
輸出文件稽揭,在此之前俺附,首先得對源文件進行預(yù)處理,把路徑寫入路由溪掀。由于Hexo本身設(shè)計的特點事镣,源文件又分為內(nèi)容和主題兩部分,分別存放在source和theme文件夾中揪胃,所以得調(diào)用process
函數(shù)分別對它們進行預(yù)處理璃哟。
function generate(hexo) {
hexo.source.process();
hexo.theme.process();
routerList.forEach(path => writeFile(path))
}
Hexo抽象出一層公用模塊用來管理所有處理器,命名為Box喊递,相當(dāng)于一個容器随闪,統(tǒng)一管理處理器的添加刪除執(zhí)行監(jiān)控,并分別為source和theme創(chuàng)建實例册舞,Box原型如下
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容器中挽荡。
module.exports = function(hexo) {
var processor = hexo.extend.processor;
var obj = require('./asset')(hexo);
processor.register(obj.pattern, obj.process); // pattern為文件名匹配格式
...
};
以markdwon文件的處理為例,成功匹配到文件擴展名后即供,調(diào)用hexo-front-matter利用正則表達式匹配來解析文件定拟,分離頂部元數(shù)據(jù)與主題內(nèi)容,類似于gray-matter逗嫡,把元數(shù)據(jù)與內(nèi)容以key/value的形式轉(zhuǎn)換為一個js對象青自。
// 處理器
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
}
}
}
}
// markdown文件
---
title: hello
layout: home
---
# Hexo
A fast, simple & powerful blog framework
解析成 =>
{
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延窜,用于在模板渲染前后修改具體的數(shù)據(jù),也可把它看成一個鉤子抹锄,例如使用marked編譯markdown文件內(nèi)容逆瑞。
hexo.execFilter('before_generate', function(data) {
hexo.render.render({
text: data._content,
path: data.source,
engine: data.engine
});
};
轉(zhuǎn)換后增加一條content屬性,帶有標(biāo)簽與類名的markdown html片段伙单。
{
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'
}
得到頁面數(shù)據(jù)后获高,進入模板引擎渲染階段,Hexo本身并不帶模板引擎的實現(xiàn)吻育,需要借助第三方庫念秧,如ejs,并通過一個適配器布疼,把原接口轉(zhuǎn)換為需求接口摊趾,向擴展對象extend.render中注冊模板解析函數(shù)。
hexo.extend.renderer.register('ejs', 'html', function(data, locals) {
require('ejs').render(data, locals))
});
模板引擎解析后的函數(shù)存儲在hexo.theme對象中缎除,以文件名作為key严就,后續(xù)渲染時只需匹配layout就能找到指定的渲染函數(shù)总寻,注入locals變量(上面markdwon解析后的js對象+擴展對象extend.helper定義的變量器罐、函數(shù)),生成最終文本字符串渐行。
var view = hexo.theme.getView(data.layout);
view.render(locals)
最后通過Nodefs
模塊把最終文本字符串輸出到public目標(biāo)文件夾中轰坊,大功告成。
回顧整個工作流程祟印,可以看作
cli => hexo init => plugin load => process => filter => render => generate
擴展閱讀
此外肴沫,Hexo還有許多優(yōu)秀的設(shè)計模式
數(shù)據(jù)庫系統(tǒng)
Hexo引入了json數(shù)據(jù)庫warehouse,也是作者自己開發(fā)的一個數(shù)據(jù)庫驅(qū)動蕴忆,API用法與Mongoose相差無幾颤芬,在架構(gòu)中的角色是充當(dāng)一個中介者,存儲臨時數(shù)據(jù),或者持久化數(shù)據(jù)存儲站蝠,如博客的發(fā)表時間等汰具,還可以作為緩存層,比對文件的修改時間菱魔,跳過無修改文件的編譯過程留荔,減少二次編譯的時間。
異步方案
大量的異步回調(diào)文件操作會讓代碼喪失可讀性澜倦,Hexo引入Promise庫bluebird聚蝶,內(nèi)置豐富的API,很方便的處理異步的流程控制藻治,如使用Promise.promisify(require('fs').readFile)
可以把原生fs
異步函數(shù)包裝成一個Promise對象碘勉,另外,隨著Node7.6的正式版發(fā)布桩卵,直接支持async/await語法恰聘,可以更優(yōu)雅得處理異步問題。
通用日志模塊
把Log劃分為六個級別吸占,'TRACE', 'DEBUG', 'INFO ', 'WARN ','ERROR','FATAL'晴叨,不同級別輸出不同的格式與顏色(chalk),并提供命令行接口矾屯,如果帶有--debug
字段兼蕊,則Log自動降級為'TRACE'級別。
End.