介绍使用 Hexo 搭建博客的经验。

我在年初把博客迁移到了 ruhoh,用了一段时间之后,也开始感觉到各种不满意的地方。

就我个人的感受,ruhoh 有以下几个不足:

  1. 页面生成速度太慢。到目前为止我的整个站点总共写了100来篇文章,在生成的时候花了将近半分钟的时间。
  2. 定制性有限。由于作者一开始就把 ruhoh 定位在 language agnostic 的目标,因此在采用的模板语言也选用了最平台无关的 mustache。但 mustache 的语法能力实在有限,只支持简单的逻辑判断。像传参、循环之类的特性都不支持。比如我想实现类似 octopress 这样的 tag 插件,直接用 mustache 是不可能的,只能动 ruhoh 的 ruby 源码,而这样又和作者的设计目标相反了。
  3. 太小众。用 ruhoh 的人太少,插件相应也少,不像 Jekyll 或 octopress 那样有丰富的扩展。
  4. 不跨平台。ruhoh 在 Windows 下总会遇到一些奇怪的问题而无法运行。如果我因为特殊原因而需要在 Windows 下写文章的话就会很不方便。
  5. 更新缓慢。在我写这篇博客之前,ruhoh 的已经半年没有什么重大更新了。我在 4 个月前给作者提交了一个给文章生成 postid 的功能性建议,到现在的标签依然还停留在 next-release 阶段。
  6. 最致命的一个问题是,是连作者自己都很少用 ruhoh!在一个 issue 页面里,作者跟我们承认他自己平常也很少使用 ruhoh 。可想而知,如果一个产品连开发者自己都不用的话,很难保证这个产品会做得好。

作者自己也很少用 ruhoh

出于以上种种原因,我决定弃用 ruhoh ,改用其他的博客系统。

我第一个尝试的是 Jekyll  Jekyll图 1 Jekyll ,因为它本身也是基于 Ruby on Rails 框架,符合我的习惯,并且它的确很受欢迎。相比 ruhoh 而言,Jekyll 就显得非常原始了。刚开始启动服务器后,整个页面几乎啥都没有。所幸用 Jekyll 的用户很多,有很多现成的案例可供参考。加上自身使用 Liquid 模板语言,扩展起来也挺方便。但正当我基本移植了原来的主题,开始预览原来写的文章后,我发现了一个问题:Jekyll 的刷新居然是每更新一篇文章就要重新编译整个网站!于是我对 Jekyll 彻底失去了使用的兴趣。之后我又尝试了 Python 系的 pelican,总体感觉也很不习惯。

后来在微博上 popozhu 向我推荐了 hexo ,一个基于 node.js 的博客系统。本来我就对 node.js 非常感兴趣,于是立马去 hexo 的主页了解更多它的特性。在看了它的文档之后,我立马把它列为我的 first choice 。与 Ruhoh 相比,hexo 有以下几个优点:

  1. 页面生成速度超快。我的100来篇文章只用了不到两秒就生成完毕。
  2. 使用 ejs 作为模板驱动,本身保留了 Javascript 语法的特性,所以定制性很强。
  3. 用户数量多,插件丰富。也许是因为作者是台北同胞的原因,项目的issue页可以看到很多中文的issue,于是就吸引了广大祖国同胞的使用 1 1不过这也带来了一个国际化的问题。作者提倡尽量使用英文来提交bug和建议,但为了照顾英文能力较弱的朋友,只好用中文和英文各写一遍。–bb 。加上作者的项目管理非常合理,不仅有比较完善的项目主页和文档,也有很好的插件wiki,然后又有 node.js 的 npm 社区支持,感觉非常 promising 。
  4. 跨平台。javascript的跨平台特性还是相当不错的,虽然我暂时还没尝试在 Windows 下跑 hexo,但从用户反馈来看应该不存在太大的问题。
  5. 更新频繁。
  6. 作者自己也用 hexo,可以看看他的博客

虽然 hexo 看起来很不错,但由于 Ruby on Rails 和 node.js 毕竟是两个不同的平台,加上两个博客系统本身设计的不同,我在试图进行移植的时候也遇到了不少困难:

更换 pandoc renderer

第一件我急着要解决的事情是将 hexo 原来的 Markdown renderer 改成用 pandoc。由于我原先的所有文章都是用 pandoc-markdown 来实现的,而自带的 renderer 则只支持最原始的 Markdown,也就是 John Gruber 定义的版本。因此直接使用自带 的 renderer 就会产生很多格式上的错误。但后来证明最急着要完成的事情往往是最难以完成的事情。在摸清楚了插件的编写方法后,我在 npm 社区上找到了一个 pandoc 的 wrapper,然后写插件调用它。但最初实现的版本工作起来很不稳定,总会随机性的出现部分页面没有渲染完整的问题。不过起码先有一个 work 的版本了,就暂时先用着它,等最后主题也移植完成了,我再回过头来解决它。于是我先给 hexo 提交了一个 issue 看看有没有人能够在我之前先解决了这个问题。

等真到了这个时候,我才发现事情没有那么简单。首先我怀疑是 hexo 的多线程编译跟 pandoc 的子进程发生了冲突,于是试着关闭 hexo 的多线程功能,并且将最大同时打开文件数设为1,但重启后发现问题依旧。之后我怀疑数据在传参过程中发生丢失,就试着把每一步都用 console.log 打印出来,发现也没有问题,只在我用回调函数返回的时候才出现随机的数据未处理情况。后来我一怒之下将原来的 wrapper 直接重写到我的插件里,也顺手把原来只能通过两层回调函数才能实现的调用改成了一层,问题终于解决了。我也就自己 close 了那个 issue,并在 npm 社区发布了一个 hexo-renderer-pandoc 插件。

wiki 页面

另外一个让我在迁移过程中感到水土不服的地方是 hexo 没有 collection 的概念。ruhoh 的一大特色是为文章提供了 collection 的抽象,每个 collection 是彼此独立的,各自提供了 categories,tags,paginations 等。因此可以很容易在同一个站点划分不同的板块。

例如,我的博客我的wiki 实际上共享了同一个 ruhoh 的站点,只不过博客里的文章属于 posts 这个 collection ,而 wiki 里的文章则属于 wiki 这个 collection ,两个板块的文章可以各自有相应的分类和标签,但不会有交叉。

而 hexo 并没有 collection 这样的概念。虽然 hexo 支持在 source 目录里放多个目录,但只有 _posts 目录的文章可以通过 site.posts 获取,其他目录的只能是一些 page。看样子只能新建一个站点用来处理 wiki ,但这样我又得为其专门写主题,而且两个站点各自都得用 hexo server 来预览 ,并占用不同的端口。更麻烦的是由于两个站点共享同一个导航栏(navigation),但两个站点在本地预览的时候不是同一个端口号,那么我还得根据当前的工作模式以及当前站点的端口号写一堆代码来实现导航栏。总而言之我非常崩溃。不过后来我想了一个workaround,因为只有我的 wiki 里的文章才有 categories 变量,其他 pages 没有这个变量。所以可以根据这个把 wiki 里的文章和其他 pages 区分开,再根据 categories 属性分类。于是我便写了段简单粗暴的代码,总算搞定了 wiki 页面。另外,原来我在 ruhoh 一直无法解决的自定义文章排序问题也在 hexo 里完美解决了。

主题

主题的移植相对来说就只是个体力活了。hexo 本身自带的 light 主题已经非常完善,但出于对 twitter-bootstrap 的好感,以及对我原来花心思写好的主题的不舍,我决定还是大刀动斧,重写了整个主题的方方面面。相比 ruhoh 而言,hexo 的主题非常模块化,由于ejs允许在不同模板间传参,所以代码的冗余度可以降低 2 2当然如果一味追求低耦合,又会带来模板文件数量太多而难以定位的缺点。 ,完全不像 ruhoh 那样,partial 和主题各自属于不同的文件夹,但是逻辑上又难以完全分离,总让我感觉很诡异。

再说说 cache 变量吧,大部分的服务器都会提供 developmentproduction 两种工作模式,比如如果当前是属于本地开发预览状态,还没有发布到远程服务器上的话,这个状态就是 development 模式,如果正在编译产生最终要发布到远程服务器的版本,系统就会把当前工作模式认定为 production 模式。利用这两个模式可以做很多有意思的事情。比如,如果我想在本地开发预览的时候使用未压缩的样式文件,而在正式编译的时候改用预先用 YUICompressor 压缩后的版本, 在 ruhoh 中可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
{{# env.production }}
bootstrap.min.css
bootstrap-responsive.min.css
style.min.css
responsive.min.css
{{/ env.production }}
{{^ env.production }}
bootstrap.css
bootstrap-responsive.css
style.css
responsive.css
{{/ env.production }}

而 hexo 虽然没有开放类似 ruhoh 的 env 接口,但由于它使用的是 express 作为服务器,可以通过 cache 变量来获取当前的模式——如果 cachetrue ,则表示当前处于 production 模式,否则则处于 development 模式。因此可以这么写:

1
2
3
4
5
6
7
8
9
10
11
<% if (cache) { %>
bootstrap.min.css
bootstrap-responsive.min.css
style.min.css
responsive.min.css
<% } else {%>
bootstrap.css
bootstrap-responsive.css
style.css
responsive.css
<% } %>

{% endraw %}

代码高亮

hexo 本身是自带代码高亮的,但 hexo 自带的代码高亮功能似乎不能完全高亮我原先用 google-prettify 高亮的代码。于是我只好写了个 google-prettify 的高亮插件,并继续沿用我稍微定制过的 tomorrow-night 主题。但这个主题和 gist 的样式会有冲突,而 gist 的样式又无法修改,无奈之下,我想通过 jquery 来动态将 gist 的样式也设为与 tomorrow-night 一致的风格,后来我发现原来已经有人写过这样的插件,叫做 pretty-gist {% mnote 3 %} 完美主义者大有人在呀,哈哈。 {% endmnote %} ,并且自身也集成了代码高亮,看了它的代码后,我稍微修改了下,让它改为使用 google-prettify 样式,最终的效果非常和谐,见上一节 #主题 的两段 gist 代码。

tag plugins

hexo 号称支持 Octopress 的所有 tag plugins,我仔细阅读代码后发现作者不过是用 javascript 移植了原本用 ruby 写的代码。仔细阅读了每个 tag plugin 的写法后,我发现这样的插件写起来也不难。比如,我的文章右侧的注解是自己实现的一个插件,可以通过下面的语法来调用:

{% raw %}

1
2
3
{% mnote index %}
Margin note string
{% endmnote %}

因为我使用 Emacs 作为主要编辑器,为了进一步减少工作量,还可以编写一个 snippet:

1
2
3
4
# name: margin-note
# key: mnote
# --
{% mnote ${2:index} %} ${1:text} {% endmnote %}$0

这样我每次要写注解的时候,只需要输入 mnote 然后敲一下空格,就可以补全成上面的插件代码,这个代码再经过编译就会展开成最终的 html 代码。关于 tag plugins 和 yasnippet 的结合可以参考这篇文章。plugin + snippet的结合非常爽快,所以我自己又写了不少插件,见这里。我打算在接下来把 twittter-bootstrap 常用的组件都写成插件,方便我使用。

后续计划

上面罗列了大大小小的各种定制,感觉都有点不务正业了。接下来就想赶紧回到正轨,开始我的毕业论文的工作,在同时把新博客用上,不断地总结我学到的新东西。当然,现在的博客还有一些不完美的地方,接下来也想试着去完成:

  1. 优化我的 mathjax 插件,在 development 模式下加载本地的 mathjax 脚本,在 production 模式下加载 CDN 上的版本。4 4这个计划被迫放弃了。Mathjax太大了,试图本地加载它会导致hexo的服务器启动半天,实在划不来。
  2. 这次博客的迁移我顺便改用了一个更好的 permalink,因此文章的 url 都发生了改变。因此我又得迁移 disqus 上的评论了 --bb
  3. hexo 会在使用过程中产生 db.json 文件,记录整个站点中的所有文章的信息。理论上可以使用 ajax 和 jquery 这个文件来检索,实现一个比 google 更好的本站搜索引擎;
  4. 用 livereload 在我编辑文章的时候自动刷新页面。