大前端-5分鐘帶你讀懂Hexo源碼設(shè)計模式

Hexo是什么?

官方定義是快速、簡潔且高效的博客框架,實際不僅僅于此奄容,它是一個JS語言編寫的靜態(tài)網(wǎng)站生成器,主要作用是解析Markdown語法产徊,并配合模板引擎昂勒,快速生成靜態(tài)網(wǎng)站。同時舟铜,還可以自定義主題戈盈,引用第三方插件,除了搭建個人博客之外,Hexo還被許許多多的項目拿來生成API文檔塘娶,如著名開源項目VueJS归斤、WeexEgg等等刁岸。

框架特色

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模塊牲证,更容易得通過onemit發(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 Functioneval來解析并執(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.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末件蚕,一起剝皮案震驚了整個濱河市孙技,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌排作,老刑警劉巖牵啦,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異妄痪,居然都是意外死亡哈雏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門衫生,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裳瘪,“玉大人,你說我怎么就攤上這事罪针∨砀” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵泪酱,是天一觀的道長派殷。 經(jīng)常有香客問我还最,道長,這世上最難降的妖魔是什么毡惜? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任憋活,我火速辦了婚禮,結(jié)果婚禮上虱黄,老公的妹妹穿的比我還像新娘悦即。我一直安慰自己,他們只是感情好橱乱,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布辜梳。 她就那樣靜靜地躺著,像睡著了一般泳叠。 火紅的嫁衣襯著肌膚如雪作瞄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天危纫,我揣著相機與錄音宗挥,去河邊找鬼。 笑死种蝶,一個胖子當(dāng)著我的面吹牛契耿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播螃征,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼搪桂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盯滚?” 一聲冷哼從身側(cè)響起踢械,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎魄藕,沒想到半個月后内列,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡背率,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年话瞧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片退渗。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡移稳,死狀恐怖蕴纳,靈堂內(nèi)的尸體忽然破棺而出会油,到底是詐尸還是另有隱情,我是刑警寧澤古毛,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布翻翩,位于F島的核電站都许,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏嫂冻。R本人自食惡果不足惜胶征,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桨仿。 院中可真熱鬧睛低,春花似錦、人聲如沸服傍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吹零。三九已至罩抗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間灿椅,已是汗流浹背套蒂。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留茫蛹,地道東北人操刀。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像婴洼,于是被迫代替她去往敵國和親馍刮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容