自从初次接触 Hexo 到现在已经有两年多的时间了,时间过得飞快啊,关于 Hexo 的优点不再赘述,关于个人站点的优点,有必要在强调一下,那就是极高的自由度,这也是这篇文章的基础。现在有时间刚好总结一下我对于 Hexo 做的一些自定义扩展,虽然之前可能在别的文章中或多或少的涉及了,但并没有统一整理过。

本人主题:Indigo,以下内容均基于此主题所写。

首先需要明白的是,Hexo 的博客内容(静态内容)均由 generate 生成,其核心是一个 node 应用,提供了一系列帮助函数,或者说调用接口;而各种主题,只不过是在其规定的框架内,以一种特定的模板(ejs/swg/pug),调用特定的 Hexo 帮助函数来完成的。在构造时,这些模板文件每次都会重新生成对应文件,例如文章页面,就是对应的模板文件将编译后的 markdown 格式的文本填入 HTML 页面,同时也会插入进去其他东西(比如题目,尾注等等)。以EJS+LESS为例:

  • EJS中包括全部的 html标签 和 JavaScript 脚本
  • Less是CSS的一种使用方式,这里可以理解为样式文件,但其样式参数可以用变量来表示,这样在开发主题的过程中就可以简化和统一整个样式所涉及的颜色高度等CSS属性。

原页面修改

由上面可知,对博客进行的任何修改,这里特指简单的、在已有界面上的修改,均需要找到渲染/生成该 HTML 页面(浏览器中我们可见的部分)的模板文件,在模板文件中进行我们想要的修改。如果涉及主题的CSS样式,则一般需要找到对应的 less 文件,如果里面用的变量代替,则还需要到存储变量值的文件里去修改变量的值,这样才完成样式的修改,但偷懒的方法是,直接在对应生成页面的模板文件中添加style代码段,然后用 !important 将你修改后的样式强制覆盖原先的主题样式。

总之,或许你的主题文件中包含很多的模板文件,但实际他们是有机的整体,都会在某个模板文件中被引用,从而组合成一个完整的整体,修改时要耐心的去找到最细粒度(对应html语句)的那部分。

新增页面

主要有两种方式,一种是添加一个 md 文件,一种是直接放一个 html 文件,前者在渲染时会生成相应的 html 内容,两者本质上没有什么区别,取决于你添加的新页面的内容,比如一般的文本则使用 md 就可以了,但如果是复杂的 js插件,那还是直接添加 html 较好,方便修改。

这两种方式均需要将 md 文件或者 html 文件放入到 Hexo 根目录的 source 文件夹中,Hexo 的机制是 source 文件夹中的全部文件都会被原封不动的生成到 public 文件夹内(只有 md 文件会被“翻译”成 html 格式)。

基于 Hexo 参数的修改

这一部分可以在某些原有页面上添加,也可以是在新增页面上添加。主要是通过借助 Hexo 的程序接口,获取像 文章数量、分类数量、各种标签下分别有多少文章等等数据,通过这些数据,可以完成一些功能,比如:

主要基于的对象:分类(Category)、标签(Tags)、文章(Article)

可以扩展的操作:过滤、匹配、重构

其实扩展内容可以由阅读原先的主题文件的代码来理解,因为包括“归档”、“分类”等页面的显示,均用的 Hexo 的帮助函数,具体的函数接口定义与说明可以参照 Hexo的API说明

案例一:添加关联文章

此处为本页面内的一张图片

扩展思路:这里使用 Tags 进行比对,相似越多的文章就越相关,在最后一步,Category 相同会优先入选,排序后只取最相关的 TOP3,不足三个则有多少显示多少;

扩展结构:

1. 找合适的文件插入实现上述功能的新函数,一般在主题的 plugin.js,之后以注册函数的形式完成代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
hexo.extend.helper.register('getTagsList', (archive, size) => {
var posts = hexo.locals.get('posts');
var articleArr = [];
var small = [];
size = size || 10;
var ishere = [];
for(var j = 0; j< archive.tags.length; j++){
var contnu = true;
var tname = archive.tags.data[j].name;
for(var p = 0; p< posts.length; p++){
var tid = posts.data[p]._id;
if(small.length == size || !contnu )
break;
for(var k = 0; k< posts.data[p].tags.length; k++){
if( tname === posts.data[p].tags.data[k].name && ishere.indexOf(tid) == -1 && archive._id != tid){
if(posts.data[p].category === archive.category && archive._id != tid){
small.push(posts.data[p]);
contnu = false;
}
articleArr.push(posts.data[p]);
ishere.push(tid);
}
}
}
}
if(small.length >= 1){
return small;
//正常情况下返回文章数组
}else{
var ln = articleArr.length;
if ( ln < size)
return articleArr.slice(0, ln-1);
else
return articleArr.slice(ln-1-size, ln-1);
}
});

2. 新建子模板文件(为了模块分离)

注意:这里的getTagsList(page, 3)就是对上述代码的调用,page即整个文章对象,可以从中获取到该文章的所属分类和标签信息,3则时控制显示相关文章个数的参数。可以发现每篇文章都需要这样查找一遍,所以该代码最后在第三步中放到了文章页的ejs模板中,以确保在渲染每篇文章时将相关文章找出并放进页面中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<section>
<p><span>「 相关文章 」</span></p>
<ul>
<%
//页面全部内容,只有TOP3
var arr = getTagsList(page, 3);
//每次生成文章都会执行
for( var i in arr) {
var article = arr[i];
%>
<li>
<a href="<%- url_for(article.path) %>"><%- article.title%></a></b>
<!--显示文章的摘要-->
<p><%- truncate(strip_html(article.excerpt || article.content ), {length: 100}) %></p>
</li>
<%
}
%>
</ul>
</section>

3. 将上述新模板内容插入到原主题的对应位置内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

<!--
这部分是整个文章页的汇总模板
-->

<%- partial('post/toc', { post: post}) %>

<article id="<%= post.layout %>-<%= post.slug %>"
class="post-article article-type-<%= post.layout %> fade" itemprop="blogPost">
<!--
post-card 即文章的主体部分,包括题目、分类、字数、及最下方的标签和分享图标等
-->
<div class="post-card">
<h1 class="post-card-title"><%- post.title %></h1>
<div class="post-meta">
<%- partial('post/date', {date_format: config.date_format}) %>
<%- partial('post/category') %>
<%- partial('post/wordcount') %>
<%- partial('plugins/page-visit') %>
</div>
<div class="post-content" id="post-content" itemprop="postContent">
<%- post.content %>
</div>
<%- partial('post/copyright') %>
<%- partial('post/reward-btn') %>
<div class="post-footer">
<%- partial('post/tag') %>
<%- partial('post/share-fab') %>
</div>
</div>
<!--
下面的模块,第一个是左右导航栏,第二个是咱们的相关文章模块,第三个是评论
最后,还包括了打赏图标(reward)在文章最底部
-->
<%- partial('post/nav') %>
<%- partial('post/postlist')%>
<%- partial('post/comment') %>
</article>
<%- partial('post/reward') %>

案例二:Category 或 Tags 的数值展示

实现思路:统计分类、标签的重复个数,并绑定其所载文章的创建日期,这样整体上就有变量值、时间值两个维度,可以借助 D3.js 等做一些复杂的分析展示模块。

假象模块:

  • 单一维度:分类及标签的名称及数量(柱状图)
  • 两个维度:文章发布的频率:日历图(类似github贡献图)
  • 复杂分析:以文章类别为一组,标签为一组,出现在同一文章中则算作有关联,分析全部文章,将分类和标签的关系用 Graph 进行可视化,从而更好的指导分类和标签的匹配关系。

实现部分:与案例一类似,第二三步骤很简单,关键是上述假象模块的实现,主要包括 通过 Hexo API 得到相关数据通过可视化库进行绘制

继续更新中

19-12-20 新问题:引入资源文件

例如想在 Hexo 博客中的某个页面,做成一个资源分享页面(自己用或者给别人用),这样就会涉及文件的下载。其实有以下几种方式实现:

  • 直接将该文件放在博客文件中一起发布,这对于单个小文件来说并无大碍,比如自己的头像完全可以这样放,但(以图片举例)图片过大,或者数量过多,就一定不能将其和其他文件放在一起,原因是:

    • Hexo 每次发布都会重新生成博客的全部静态页面,如果资源也放在其中,那么也会每次都刷新一遍,如果是部署在云端或者用Pages服务进行托管的,这样每次的上传量将非常庞大,上传时间会非常长,且浪费时间的恰恰是基本不会修改的资源文件。
  • 将资源文件分离,放在其他地方(云空间或Github的其他仓库)。例如使用七牛云进行存储,这样的好处是:

    • 访问速度有保证,例如七牛云就有融合CDN加速
    • 对资源的可控性高,可以知道请求的数量等指标,但如果自己去计算这些数据,则需要复杂的代码实现。
    • 和 Hexo 分离,加快每次的上传更新速度

其实这个问题适用于博客中所涉及的全部资源文件,包括头像、文章中插入的照片等等。七牛云是个不错的对象存储平台(包括文档、图片、媒体文件等等),如果只是图片的话,推荐使用专职的图床进行存储(例如微博图床)。