老规矩,我们先来看结论。

挑选Hakyll的好处:

  • 跨平台,实测支持Windows
  • 利用Pandoc转换文档格式,支持格式多,速度快
  • 笔者更喜欢使用MediaWiki或LaTeX格式写文章
  • 提供灵活的过程函数,自由组合构建过程,即无穷的可能性

选择MediaWiki格式

理由很简单,笔者写维基百科,也使用自己搭建的MediaWiki来记录平时遇到的问题。笔者认为,Markdown的流行完全是因为GitHub的流行。Markdown固然可以应付简单文档的需求,并且原格式几乎不需要学习就能看懂(参考这个例子),然而难以忽视的是一些常用文本格式,原始Markdown是不支持的,即使加上GitHub其他实现的私货后仍然算不上好用。

同理,另外一种为诸多文档所使用的ReStructedText格式不但使用复杂,原文件也相当难看(参考这个例子)。

例如我们会想要标题目录,脚注,表格等等。既然我们已经学会HTML,为何还要再学一种复杂程度相当且不带来更多好处的标记呢?

笔者固执地认为这些标记格式的表现能力始终无法与TeX匹敌,虽然笔者现在不会,但保留对其学习使用的可能。

而支持这些格式,转换效率又高的工具,Pandoc是比较合适的(笔者的设备上140篇文档转换消耗了4.3秒,CPU为i7-4710MQ)。

选择Hakyll

在选用Hakyll前,笔者曾纠结过是否利用最熟悉的Python语言自己实现一个,不过很快放弃。原因从头折腾一个前辈们已经做烂的东西必要性不大;最重要的格式转换部分还是得依赖其他工具(笔者甚至考虑过如何让Python调用Haskell的API),程序间交互性能会有损失;自己做一个并不一定能比现有工具做的好(想想程序员们热爱的DRY原则)。

此外,Hakyll提供的过程工具相对强大,类似XMonad(一款强大的平铺窗口管理器,笔者将花时间介绍),只要会Haskell就可以自由组合而避免重写大量代码。

Haskell(或函数式编程)的哲学之一是将功能单一的函数组合起来,构成复杂的流程

动手玩

搭建Haskell运行环境,请参考笔者先前写过的文章:

Haskell、GHC与Cabal简单上手

如果第一次使用Hakyll,可以考虑使用自带的例子:

$ hakyll-init my-site

或者可以参考作者给的上手例子

这里我们以本站为例(源代码看这里),简要介绍一下主要流程与功能实现。

目录结构

.
├── bower_components             # bower第三方JavaScript与CSS组件
│   ├── normalize.css
│   └── unsemantic
├── _cache
├── css                          # 自己写的CSS
├── dist                         # Hakyll构建时的目录,可以忽略
│   ├── build
│   └── dist-sandbox-39cb48b3
├── node_modules                 # NodeJS模块,这里仅用到了bwoer
│   └── bower
├── posts -> ../../posts         # 文章目录,由于不喜欢放在一个目录这里采用符号链接
├── _site                        # 最终构建完成后的网站根目录
│   ├── css
│   └── posts
└── templates                    # 页面的模板文件

文章

很多静态网站构建工具在托管站点源代码方面别提到这点,而是把工具、模板样式与文章混在一起。笔者认为这种做法不合理,有的时候我们会可能使用其他工具转换,所以把文章分开管理。这里可以使用符号链接,或者git submodule也是一种方案。

页面模板

页面模板仅依靠Pandoc的标记无法做复杂的事情,不过对于简单的功能(输出变量内容,if判断,循环)来说足够了。

更多的自定义可以考虑使用其他过滤器,在构建过程中自定义(Hakyll的强大之处就在这里)。

模板间的套用关系,将在构建配置site.hs中指定。

JavaScript与CSS资源

没有接触过NodeJS前端的各位,是不是依旧在把第三方资源的源代码放到自己的版本管理中呢?其实已经不必啦。

容笔者简单介绍一下Bower这款工具,既然各大语言都有自己的代码分发机制,静态资源为何不可呢?利用Bower就可以在指定的配置文件bower.json中申明项目的依赖,jQuery?Bootstrap?都不是问题。

利用Bower下载后的资源放在bower_components中,这里会有个问题我们最终部署的站点可不能出现诸如

这样的URL路径,着实太难看,也显得不够专业。那么除了使用诸多框架提供的静态资源管道(assets piping*)功能外,静态站点如何应对呢?答曰:符号链接

css
  normalize.css -> ../bower_components/normalize.css/moralize.css

注assets piping: 这个功能据传最早是Rails框架的作者D.H.H.提出的,目前重视前端的框架中都会有支持,其理念就是将静态JavaScript与CSS经过编译再输出到前端的文件资源,那么源代码采用何种形式写都不是问题了,还能自由组合;这也就是SASSCoffeeScript这类变种诞生的前提。

:笔者选择unsemantic作为网格布局系统的原因是足够简单也足够满足需求,诸如Bootstrap与Foundation都没有将布局系统剥离出来,而能搜到的多数实现有些想过了头,不是类名命名不好看,就是类名污染。

构建与发布

Hakyll的配置描述用Haskell源代码编写,本站的配置在原始例子的基础上修改的来:

--------------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings #-}
import           Data.Monoid (mappend)
import           Hakyll


--------------------------------------------------------------------------------
main :: IO ()
main = hakyll $ do
    match "images/*" $ do           -- 指定图片全复制
        route   idRoute
        compile copyFileCompiler

    match "css/*" $ do              -- 指定CSS文件压缩
        route   idRoute
        compile compressCssCompiler

    match "posts/**" $ do           -- 指定文章路径和目录结构一致
        route $ setExtension "html"
        compile $ pandocCompiler
            >>= loadAndApplyTemplate "templates/post.html"    postCtx  -- 套用模板
            >>= loadAndApplyTemplate "templates/default.html" postCtx
            >>= relativizeUrls

    create ["archive.html"] $ do    -- 归档页面,列出所有文章
        route idRoute
        compile $ do
            posts <- recentFirst =<< loadAll "posts/**"
            let archiveCtx =
                    listField "posts" postCtx (return posts) `mappend`
                    constField "title" "Archives"            `mappend`
                    defaultContext

            makeItem ""
                >>= loadAndApplyTemplate "templates/archive.html" archiveCtx
                >>= loadAndApplyTemplate "templates/page.html" archiveCtx
                >>= loadAndApplyTemplate "templates/default.html" archiveCtx
                >>= relativizeUrls


    match "index.html" $ do         -- 首页
        route idRoute
        compile $ do
            posts <- recentFirst =<< loadAll "posts/**"
            let indexCtx =
                    listField "posts" postCtx (return
                                               (take 10 posts)) `mappend`  -- 只显示最近10篇
                    constField "title" "Home"                `mappend`
                    defaultContext

            getResourceBody
                >>= applyAsTemplate indexCtx
                >>= loadAndApplyTemplate "templates/default.html" indexCtx
                >>= relativizeUrls

    match "templates/*" $ compile templateCompiler      -- 加载模板


--------------------------------------------------------------------------------
postCtx :: Context String
postCtx =
    dateField "date" "%Y-%m-%d" `mappend`  -- 指定日期格式
    defaultContext

由于Haskell是静态编译的语言,所以每次修改完配置,记得要重新编译并安装命令:

$ cabal install

构建静态站点:

$ site clean        # 清理临时与目标文件
$ site build        # 构建静态站点
$ site serve        # 启动HTTP服务端用以浏览效果
$ site watch        # 同serve,并且监视文章目录,有修改便重新构建相应文件

特别提示:在Windows下使用Hakyll也是可行的,除了必须使用命令提示符(cmd)或PowerShell外,还要注意locale。切换方法如下:

$ chcp 10036  # 即UTF-8

最终生成的站点文件在_site下,使用rsync手段进行发布即可。

还有不满意?

没有做不到,只有想不到。由于构建的配置都是由Haskell源代码写成的,所以拥有无限多种组合的可能性,需要的就是想象力。可以从各位巨巨的Hakyll配置中得到启发。

阅读Hakyll的源代码文档,了解熟悉更多功能。

笔者能想到的一些常用的可能改进有

  • CSS利用SASS编译?
  • 增加模板文件中的标签定义,组合、展开成更复杂输出,例如原生的标记语言无法实现的功能等等?
  • 添加文章的元信息,例如标签(tag)与分类(category),并在页面上输出等?
  • 定制URL样式?
  • RSS源?
  • 标题目录支持?
  • 脚注支持?
  • 多语种支持?

静态网站构建工具?新瓶老酒

静态网站构建工具早已不是什么新鲜事,近几年最广为人知的当属于JekyllOctopress。其实这个事情的原理就是利用自己喜欢的格式来写页面内容,通过构建过程来生成最终展现给读者的格式。回想一下,1970年代诞生的TeX是如出一辙的,写TeX描述内容与格式,再通过构建过程来生成最终的文件(例如PDF等),再想想上个世纪许多大学(计算机系)毕业生的毕业论文、简历等都是通过TeX形式写成的,所以这样看来,静态网站构建工具也不过是新瓶装老酒而已。

程序员圈子里有个怪毛病,就是好用自己喜欢的语言来实现相同的功能。之前有著名的pastebin例子(你能找到有多少种知名的pastebin实现吗?)。

这不,在基于Ruby的Jekyll与Octopress流行起来后,有人因为转换格式的效率,而更多的人则因为喜欢其他语言,所以各自脑洞大开,写起了自己的版本。还有人专门设置了一个域名的页面来罗列出已知的类似项目。

总结

如果说Jekyll打开了众人的思路,让申明式的配置限制了发挥。那么Octopress集成的众多功能与插件满足了大多数程序员与技术爱好者的写网志需求。

笔者曾经尝试过使用Jekyll写一个支持多语言的网志,但最终淹没在monkey patch中。

然而面对真正的构建需求,如同Hakyll这样类DSL形式的配置与强大工具的组合才真正带来了无穷的可能性。Hakyll从众多的静态网站构建工具中脱颖而出,不仅仅提供了“约定俗称”的构建方式,

如果现在让笔者3年前使用Ruby写一个类似的工具,一定会采用Rake的DSL方式来进行构建过程的描述。

Hakyll也许是目前对笔者来说最合适,也是最强大的工具了;不必总是花时间纠结配置的形式,自由地组合构建过程,让人爱不释手。

参考资料

__END__