Hexo 主题开发之自定义模板
關(guān)于 Hexo 如何開發(fā)主題包的教程在已經(jīng)是大把的存在了,這里就不在贅述了。這邊文章主要講的是作為一個(gè)主題的開發(fā)者,如何讓你的主題具有更好的擴(kuò)展性,在用戶自定義修改主題后,能夠更加平易升級(jí)主題。
問題所在
Hexo 提供兩種方式安裝主題包:
- 直接在 themes 目錄下直接存放主題包文件,這種方式方便用戶自己魔改主題,魔改后升級(jí)主題會(huì)比較困難
- 通過 npm 安裝主題包,這種方式更加方便用戶升級(jí)主題,但是不易擴(kuò)展
當(dāng)用戶想要自定義修改主題時(shí),基本上只能通過第一種方式安裝,然后通過修改 源代碼 形式去修改主題。這樣帶來(lái)的問題就是,當(dāng)主題修復(fù)一些 bug 或者主題迭代 N 個(gè)版本后,用戶想升級(jí)主題時(shí)就會(huì)變的比較麻煩。
有沒有能讓用戶方便升級(jí),又能提供一定個(gè)性化的能力的?答案是有的,那就是通過 npm 方式分發(fā)主題包,然后通過一些魔法,讓其有一定的擴(kuò)展能力,這篇文章就來(lái)講解如何實(shí)現(xiàn)它。
模板
在 Hexo 中,主題的模板決定的網(wǎng)站頁(yè)面程序的方式,當(dāng)你不同頁(yè)面結(jié)構(gòu)很相似時(shí)候,可以通過布局(Layout)去復(fù)用相同的結(jié)構(gòu),而相似的部分可以抽離成通用局部模板,通過使用 Partial 去加載,以達(dá)到模板復(fù)用的效果。
這就是 Hexo 在開發(fā)主題處理模板復(fù)用的方式,可把一個(gè)個(gè)局部模板理解為一個(gè)個(gè)獨(dú)立的組件,哪里需要是就在哪里加載它。如果說(shuō)把用戶想替換某一個(gè)局部模板,然后讓用戶提供一個(gè)新的模板,然后我們?nèi)ゼ虞d這個(gè)新的模板,那是不是達(dá)到在用戶不修改源代碼情況下對(duì)主題進(jìn)行個(gè)性話的擴(kuò)展呢。
Partial
要想知道 Hexo 是如果加載局部模板的,我們翻看下 Hexo 源碼里 Partial 的實(shí)現(xiàn)(/plugins/helper/partial.js),可以看到當(dāng)通過調(diào)用 ctx.theme 獲取到對(duì)應(yīng)的 view,然后調(diào)用 render 渲染的。
const { dirname, join } = require("path");
module.exports = (ctx) =>
function partial(name, locals, options = {}) {
const viewDir = this.view_dir;
const currentView = this.filename.substring(viewDir.length);
const path = join(dirname(currentView), name); // 根據(jù)當(dāng)前路徑找到,局部模板路徑
const view = ctx.theme.getView(path) || ctx.theme.getView(name); // 根據(jù)路徑去匹配 view
const viewLocals = { layout: false };
// Partial don't need layout
viewLocals.layout = false;
return view.renderSync(viewLocals);
};
Hexo 對(duì)文件處理分為兩種,一種是 source 目錄文件處理,一種是對(duì)主題包里文件處理。在輔助函數(shù)注冊(cè)里可以看 ctx 其實(shí)就是 hexo 運(yùn)行時(shí)的實(shí)例,上面的 ctx.theme 就是主題文件處理的 Box。通過 Hexo 提供 api 可以看到,它不僅提供了 getView,還提供了 setView、removeView 方法。
然后翻看 setView 代碼,可以看到當(dāng)你重新設(shè)置一個(gè)新的 view 時(shí),它會(huì)覆蓋掉已有的 view。也就是說(shuō)我們可以直接覆蓋主題里的 局部模板
setView(path, data) {
const ext = extname(path);
const name = path.substring(0, path.length - ext.length);
this.views[name] = this.views[name] || {};
const views = this.views[name];
views[ext] = new this.View(path, data);
}
修改示例
我們以覆蓋 hexo-theme-async 為示例,在生成前鉤子 generateBefore 里,覆蓋掉主題里默認(rèn)的側(cè)欄模板。
hexo.on("generateBefore", () => {
hexo.theme.setView("_partial/sidebar/index.ejs", "<div>111</div>");
});
運(yùn)行起來(lái)會(huì)發(fā)現(xiàn)側(cè)欄模板已經(jīng)替換成我們寫的 111 了。
主題實(shí)現(xiàn)
通過上面方式確實(shí)可以達(dá)到覆蓋主題默認(rèn)模板能力,但是讓用戶直接修改會(huì)很不友好,需要自己去看主題中局部模板的路徑信息,并且還需要自己編寫加載文件內(nèi)容,覆蓋主題默認(rèn)模板邏輯。
我們可以將這部分操作內(nèi)置進(jìn)入主題內(nèi),然后只需要讓用戶編寫自己的模板,以及告訴我們需要替換對(duì)應(yīng)模板即可。大致流程如下:
我們還可以提供默認(rèn)配置,簡(jiǎn)化通過路徑覆蓋
可以通過在配置中配置好主題中使用的局部模板,類似這樣,將主題中使用的局部模板以配置形式展示。
layout:
path: layout
# layout
main: _partial/main
header: _partial/header
banner: _partial/banner
sidebar: _partial/sidebar/index
footer: _partial/footer
然后在加載局部模板時(shí),直接讀取配置的信息,當(dāng)用戶覆蓋掉了 layout.header 時(shí)候,主題就會(huì)自動(dòng)使用新的模板了。
<%- partial(theme.layout.header) %>
模板加載實(shí)現(xiàn)
根據(jù)上面配置,約定 layout.path 配置指向目錄為用存在模板目錄,以便可以自定義存放路徑。
layout:
path: layout
首先就是根據(jù)配置獲取模板存在的絕對(duì)路徑,可以根據(jù) hexo 實(shí)例,獲取到根目錄,拼接出完整路徑位置。
const { resolve } = require("path");
const layoutDir = resolve(hexo.base_dir, hexo.theme.config.layout.path);
然后是對(duì)文件目錄的監(jiān)聽,這個(gè)可以直接使用 hexo-fs ,避免安裝額外的依賴包,提供了新增、刪除、修改、文件夾變動(dòng)的監(jiān)聽,可以針對(duì)不同事件做出不同操作。
const { watch } = require("hexo-fs");
watch(layoutDir, {
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 200,
},
}).then((watcher) => {
watcher.on("add", (path) => /** 設(shè)置模板 */);
watcher.on("change", (path) => /** 設(shè)置模板 */);
watcher.on("unlink", (path) => /** 移除模板 */);
watcher.on("addDir", (path) => /** 添加文件夾,遞歸遍歷設(shè)置模板 */);
});
因?yàn)槲覀兩厦媸峭ㄟ^配置去加載模板的,所有為了避免用戶自定義的模板名稱會(huì)與主題的模板名稱沖突,導(dǎo)致覆蓋了主題的模板,我們可以在使用時(shí)增加一個(gè)約定的前綴,避免重名。我們對(duì)設(shè)置模板進(jìn)行簡(jiǎn)單封裝
const setView = (fullpath) => {
const path = "async" + fullpath.replace(layoutDir, ""); // 約定固定前綴為 async
hexo.theme.setView(path, readFileSync(fullpath, { encoding: "utf8" }));
};
上面處理方式,用戶自定義模板,可以正常加載使用的,但是當(dāng)自定義的模板又引入了其他模板時(shí)會(huì)存在一個(gè)問題,在有的模板引擎中會(huì)出現(xiàn)路徑不正常。通過查看 view 實(shí)例信息,可以看到其指向目錄是在 node_modules,而實(shí)際上是存在根目錄的。
翻看 view 源碼可以看到 source 是獲取的 this._theme.base ,而 this._theme.base 往上找就 theme_dir,也就是主題存放的目錄,最后又通過 renderer.compile 設(shè)置模板渲染到,導(dǎo)致傳入 path 不正確。
知道了原因我對(duì)上面代碼進(jìn)行修正,設(shè)置后重新獲取到 view,然后手動(dòng)根據(jù)路徑信息。
const setView = (fullpath) => {
const path = "async" + fullpath.replace(layoutDir, ""); // 約定固定前綴為 async
hexo.theme.setView(path, readFileSync(fullpath, { encoding: "utf8" }));
const view = hexo.theme.getView(path);
view.source = fullpath; // 修正原文件路徑
view._precompile(); // 重新調(diào)用渲染器的初始化
};
然后將上面操作,放置在在 Hexo 的 generateBefore 中:
const { resolve } = require("path");
const { watch, readdirSync, statSync } = require("hexo-fs");
hexo.on("generateBefore", () => {
const layoutDir = resolve(hexo.base_dir, hexo.theme.config.layout.path);
const setView = (fullpath) => {
const path = "async" + fullpath.replace(layoutDir, ""); // 約定固定前綴為 async
hexo.theme.setView(path, readFileSync(fullpath, { encoding: "utf8" }));
const view = hexo.theme.getView(path);
view.source = fullpath; // 修正原文件路徑
view._precompile(); // 重新調(diào)用渲染器的初始化
};
watch(layoutDir, {
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 200,
},
}).then((watcher) => {
watcher.on("add", (path) => setView(path));
watcher.on("change", (path) => setView(path));
watcher.on("unlink", (path) => {
const path = "async" + path.replace(layoutDir, "");
hexo.theme.removeView(path);
});
watcher.on("addDir", (path) => loadDir(path));
});
const loadDir = (base) => {
let dirs = readdirSync(base);
dirs.forEach((path) => {
const fullpath = resolve(base, path);
const stats = statSync(fullpath);
if (stats.isDirectory()) {
loadDir(fullpath);
} else if (stats.isFile()) {
setView(fullpath);
}
});
};
loadDir(layoutDir);
});
到此主要功能以及實(shí)現(xiàn)了,其他待優(yōu)化項(xiàng)這里就不描述了,可以看看完整實(shí)現(xiàn)源碼。
使用示例
以為 hexo-theme-async 為例,在根目錄新建 layout 目錄,然后添加 sidebar.ejs 文件,結(jié)構(gòu)如下:
┌── blog
│ └── layout
│ └── sidebar.ejs
│ └── scaffolds
│ └── source
│ └── themes
sidebar.ejs 添加一點(diǎn)內(nèi)容
<div>111</div>
然后在 _config.async.yml 中修改 layout 配置,替換掉默認(rèn) sidebar 模板。
layout:
sidebar: async/sidebar
運(yùn)行起來(lái)后,可以看到效果和 修改示例 中的效果一樣,但是簡(jiǎn)化了用戶使用。
結(jié)語(yǔ)
通過上面方式,可以在使用 npm 安裝主題時(shí),也支持自定義替換部分區(qū)域,來(lái)個(gè)性化的目的,當(dāng)主題版本迭代升級(jí)后,也更方便用戶更新升級(jí)。
完整實(shí)現(xiàn)源碼可以參考 hexo-theme-async 中源碼。
總結(jié)
以上是生活随笔為你收集整理的Hexo 主题开发之自定义模板的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 动态规划——流水作业调度问题
- 下一篇: 【纯手工打造】时间戳转换工具(pytho