首页 > 其他分享 >Hexo 主题开发之自定义模板

Hexo 主题开发之自定义模板

时间:2023-12-16 14:44:35浏览次数:25  
标签:const Hexo hexo 主题 theme path 模板 自定义

关于 Hexo 如何开发主题包的教程在已经是大把的存在了,这里就不在赘述了。这边文章主要讲的是作为一个主题的开发者,如何让你的主题具有更好的扩展性,在用户自定义修改主题后,能够更加平易升级主题。

问题所在

Hexo 提供两种方式安装主题包:

  • 直接在 themes 目录下直接存放主题包文件,这种方式方便用户自己魔改主题,魔改后升级主题会比较困难
  • 通过 npm 安装主题包,这种方式更加方便用户升级主题,但是不易扩展

当用户想要自定义修改主题时,基本上只能通过第一种方式安装,然后通过修改 源代码 形式去修改主题。这样带来的问题就是,当主题修复一些 bug 或者主题迭代 N 个版本后,用户想升级主题时就会变的比较麻烦。

有没有能让用户方便升级,又能提供一定个性化的能力的?答案是有的,那就是通过 npm 方式分发主题包,然后通过一些魔法,让其有一定的扩展能力,这篇文章就来讲解如何实现它。

模板

在 Hexo 中,主题的模板决定的网站页面程序的方式,当你不同页面结构很相似时候,可以通过布局(Layout)去复用相同的结构,而相似的部分可以抽离成通用局部模板,通过使用 Partial 去加载,以达到模板复用的效果。

这就是 Hexo 在开发主题处理模板复用的方式,可把一个个局部模板理解为一个个独立的组件,哪里需要是就在哪里加载它。如果说把用户想替换某一个局部模板,然后让用户提供一个新的模板,然后我们去加载这个新的模板,那是不是达到在用户不修改源代码情况下对主题进行个性话的扩展呢。

Partial

要想知道 Hexo 是如果加载局部模板的,我们翻看下 Hexo 源码里 Partial 的实现(/plugins/helper/partial.js),可以看到当通过调用 ctx.theme 获取到对应的 view,然后调用 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); // 根据当前路径找到,局部模板路径
        const view = ctx.theme.getView(path) || ctx.theme.getView(name); // 根据路径去匹配 view
        const viewLocals = { layout: false };
        // Partial don't need layout
        viewLocals.layout = false;
        return view.renderSync(viewLocals);
    };

Hexo 对文件处理分为两种,一种是 source 目录文件处理,一种是对主题包里文件处理。在辅助函数注册里可以看 ctx 其实就是 hexo 运行时的实例,上面的 ctx.theme 就是主题文件处理的 Box。通过 Hexo 提供 api 可以看到,它不仅提供了 getView,还提供了 setViewremoveView 方法。

然后翻看 setView 代码,可以看到当你重新设置一个新的 view 时,它会覆盖掉已有的 view。也就是说我们可以直接覆盖主题里的 局部模板


  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 里,覆盖掉主题里默认的侧栏模板。

hexo.on("generateBefore", () => {
    hexo.theme.setView("_partial/sidebar/index.ejs", "<div>111</div>");
});

运行起来会发现侧栏模板已经替换成我们写的 111 了。

示例

主题实现

通过上面方式确实可以达到覆盖主题默认模板能力,但是让用户直接修改会很不友好,需要自己去看主题中局部模板的路径信息,并且还需要自己编写加载文件内容,覆盖主题默认模板逻辑。

我们可以将这部分操作内置进入主题内,然后只需要让用户编写自己的模板,以及告诉我们需要替换对应模板即可。大致流程如下:

demo

我们还可以提供默认配置,简化通过路径覆盖

demo

可以通过在配置中配置好主题中使用的局部模板,类似这样,将主题中使用的局部模板以配置形式展示。

layout:
    path: layout
    # layout
    main: _partial/main
    header: _partial/header
    banner: _partial/banner
    sidebar: _partial/sidebar/index
    footer: _partial/footer

然后在加载局部模板时,直接读取配置的信息,当用户覆盖掉了 layout.header 时候,主题就会自动使用新的模板了。

<%- partial(theme.layout.header) %>

模板加载实现

根据上面配置,约定 layout.path 配置指向目录为用存在模板目录,以便可以自定义存放路径。

layout:
    path: layout

首先就是根据配置获取模板存在的绝对路径,可以根据 hexo 实例,获取到根目录,拼接出完整路径位置。

const { resolve } = require("path");
const layoutDir = resolve(hexo.base_dir, hexo.theme.config.layout.path);

然后是对文件目录的监听,这个可以直接使用 hexo-fs ,避免安装额外的依赖包,提供了新增、删除、修改、文件夹变动的监听,可以针对不同事件做出不同操作。

const { watch } = require("hexo-fs");

watch(layoutDir, {
    persistent: true,
    awaitWriteFinish: {
        stabilityThreshold: 200,
    },
}).then((watcher) => {
    watcher.on("add", (path) => /** 设置模板 */);
    watcher.on("change", (path) => /** 设置模板 */);
    watcher.on("unlink", (path) => /** 移除模板 */);
    watcher.on("addDir", (path) => /** 添加文件夹,递归遍历设置模板 */);
});

因为我们上面是通过配置去加载模板的,所有为了避免用户自定义的模板名称会与主题的模板名称冲突,导致覆盖了主题的模板,我们可以在使用时增加一个约定的前缀,避免重名。我们对设置模板进行简单封装

const setView = (fullpath) => {
    const path = "async" + fullpath.replace(layoutDir, ""); // 约定固定前缀为 async
    hexo.theme.setView(path, readFileSync(fullpath, { encoding: "utf8" }));
};

上面处理方式,用户自定义模板,可以正常加载使用的,但是当自定义的模板又引入了其他模板时会存在一个问题,在有的模板引擎中会出现路径不正常。通过查看 view 实例信息,可以看到其指向目录是在 node_modules,而实际上是存在根目录的。

view

翻看 view 源码可以看到 source 是获取的 this._theme.base ,而 this._theme.base 往上找就 theme_dir,也就是主题存放的目录,最后又通过 renderer.compile 设置模板渲染到,导致传入 path 不正确。

view-code

知道了原因我对上面代码进行修正,设置后重新获取到 view,然后手动根据路径信息。

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(); // 重新调用渲染器的初始化
};

然后将上面操作,放置在在 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(); // 重新调用渲染器的初始化
    };

    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);
});

到此主要功能以及实现了,其他待优化项这里就不描述了,可以看看完整实现源码。

使用示例

以为 hexo-theme-async 为例,在根目录新建 layout 目录,然后添加 sidebar.ejs 文件,结构如下:

┌── blog
│   └── layout
│          └── sidebar.ejs
│   └── scaffolds
│   └── source
│   └── themes

sidebar.ejs 添加一点内容

<div>111</div>

然后在 _config.async.yml 中修改 layout 配置,替换掉默认 sidebar 模板。

layout:
    sidebar: async/sidebar

运行起来后,可以看到效果和 修改示例 中的效果一样,但是简化了用户使用。

结语

通过上面方式,可以在使用 npm 安装主题时,也支持自定义替换部分区域,来个性化的目的,当主题版本迭代升级后,也更方便用户更新升级。

完整实现源码可以参考 hexo-theme-async 中源码。

标签:const,Hexo,hexo,主题,theme,path,模板,自定义
From: https://www.cnblogs.com/nextl/p/17904821.html

相关文章

  • C++: 智能指针的自定义删除器 `Custom Deleter` 有什么用?
    C++11智能指针std::shared_ptr和std::unique_ptr都支持自定义删除器,本文将介绍自定义删除器的使用场景和使用方法。智能指针模板参数的第二个类型是删除器,一般是一个函数指针类型或者是一个函数对象类型。通常情况下,删除器的类型是std::default_delete<T>,它是一个函数对象类型,用于......
  • 考研保研、夏令营推免的简历模板
      本文介绍在保研夏令营、考研复试等场景中,个人简历的制作模板与撰写注意事项。  这里就将当初我自己的简历分享一下,供大家参考。其实我的简历是那种比较简单、质朴的,通篇就一个颜色,没有太多花里胡哨的部分。我个人感觉,对于读研、升学而言,其实这种简单一些的简历还是比较容易......
  • freemarker模板数据解析1--页面语法直接获取
    freemarker模板数据解析1--页面语法直接获取​ 对后端接口传递过来的Map<String,List>类型数据的展示。对于普通Map<String,String>数据,我们很清楚,直接上代码:·接口提供数据:Map<String,String>map=newHashMap<>();map.put("k1","v1");map.put("k2","v2"......
  • 模板方法模式
    模板方法模式是指定义一个操作中算法的骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下即可重定义该算法的某些特定步骤。模板方法主要有两个角色,一个抽象类模板,定义好骨架,第二是实现的子类,实现抽象类模板的抽象方法,和根据需要重写一些方法,其实这也是我......
  • ffmpeg 添加自定义编解码插件
    有两种方法:一.ffmpeg添加自定义编解码插件(以修改ffmpeg源码的方式添加)例:添加一个解码器,给这个解码器取个名字叫mydecoder,可以通过下面的步骤添加:1.在libavcodec目录下,新建文件mydecoder.c#include"avcodec.h"#include"codec_internal.h"//自己封装的编解码器的头文件#......
  • 自定义委托01
    usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;namespaceeventTest20231209{classProgram{staticvoidMain(string[]args){EventClassmy=newEventClass();......
  • php tp框架 自定义日志
    调用方法$file_log=['order_id'=>123,];(newLogs('log'))->infos('日志文案',$file_log);[2023-12-1415:24:13][INFO][log]{"msg":"日志文案","params":{"order_id":123},"file......
  • NetSuite 开发日记:如何管理多环境自定义列表值
    在NetSuite中可以创建自定义列表,列表可用于为其他(自定义)记录上的下拉选项列表值。varrec=record.create({type:'customrecord_xx'});rec.setValue({fieldId:'custrecord_xx_fld',value:'1'});rec.save();我们设置自定义列表值,需要使用该值的内......
  • MUI增加自定义icon图标
    mui框架遵循极简原则,在icon图标集上也是如此,mui仅集成了原生系统中最常用的图标;使用icon图标集的优点:多个图标字体合成一个字体文件,避免每张图片都需要联网请求;字体可任意缩放,而图片放大会失真、缩小则浪费像素;可通过css任意改变颜色、设置阴影及透明效果;一、操作方法:(1)找到任意矢量......
  • 实验6 模板类、文件I/O和异常处理
    实验任务4:1.代码:vector.hpp:1#pragmaonce2#include<iostream>3#include<stdexcept>4usingstd::cout;5usingstd::endl;67template<typenameT>8classVector{9public:10Vector(Tn);11Vector(Tn,Tvalue);12Vector(c......