一起写一个静态博客生成器

frozenblue

前言

作为一名技术宅, 平时除了写代码就是写写博客. 搭建博客一般有三种途径: 1~用CSDN等公司提供的博客; 2~自己动手; 3~基于github等git服务商提供的静态站点搭博客.
第一种博客主题有点丑(⊙o⊙)…而且不能定制, 否决掉; 第二种我试过, 首先买个服务器+域名一个月就要不少钱, 而且数据要自己存储, 更麻烦的是每次部署都要手动将博客传到服务器上, 也放弃了; 第三种方法就方便多了, 首先域名github.io就解决了, 而且生成的静态化站点托管在github的服务器上, 也不需要自己买服务器, 部署博客也方便, git add/commit/push 3步搞定!
现在最流行的静态博客生成器应该是hexo. 之前还是Mac的时候用的就是hexo, 后来Mac进水了换到kali linux, hexo装了半晚上总是报错. 最终下定决心自己写一个静态博客生成器.

什么是静态博客生成器?

静态博客生成器是一个自动化工具, 提供简单的命令行接口执行博客生成; 博客静态化; 博客部署等任务, 并向用户提供配置接口以自定义博客. 静态博客生成器同github pages服务一起, 让博客的生成和部署变得极其方便.

希望实现的效果

最终做出来的是一个命令行工具, 起名叫railgun(电磁炮), 她的API如下:

本地博客提供配置文件供用户填写基本信息、部署地址等配置项.

思路

如果上来就从静态化模块开始码会不知从何下手. 其实静态博客生成器听起来很高大上, 但她最终生成的不过就是一个博客, 最终部署的也还是这个博客. 所以不如先把这个博客搭建运行起来, 然后再想办法静态化这个博客, 最终把这些步骤构建成自动化的命令行工具, 不就搞定了吗O(∩_∩)O!

实现本地博客

本地博客不会太复杂, 因为最终需要静态化, 所以数据库什么的是不需要的. 我这里选择用Flask搭建本地博客.
选择Flask的原因有三: 一是发现了一个十分强大的Flask扩展Flask-FlatPages, 使用这个扩展可以很方便的管理各种格式的博客文件(md, rst); 二是Flask强大的路由系统方便了静态生成器的实现; 三是Flask内置的前端模板Jinja2语法简单、优雅, 方便用户(特别是非前端程序员)自定义主题.
当然, Flask也有缺点, 考虑静态生成器静态化速度的时候会说(⊙﹏⊙)b
本地博客的目录结构如下:

pages目录存放用户编写的博客文件, 博客格式可以在config.py中进行配置. config.py有如下配置项:

配置项以字典的形式汇总, 传入所有页面:

这样用户就可以在自定义主题的过程中任意使用这些配置.

接下来说说本地博客的核心插件: Flask-FlatPages. 因为是静态化博客, 所以没有数据库, 那么如何方便的对博客文件集合进行查找和访问呢?

Flask-FlatPages provides a collections of pages to your Flask application.
Pages are built  from “flat” text files as opposed to a relational database.

Flask-FlatPages完美的符合了我们的需求! 使用起来很简单:

上面这个视图函数实现了通过相对路径查找某篇博客并传入前端模板进行渲染的功能,

post.html | safe 自动过滤掉不安全字符并将html渲染为markdown.

OK! 现在我们的本地博客实现了配置接口、博客文件管理和访问,

$ python manage.py runserver

便可运行这个博客.

接下来我们要开始思考如何静态化这个博客啦!

实现静态生成器

什么是静态化?!

我们搭建的本地博客是不能直接部署在github pages服务上的, 因为我们的博客是Python程序需要启动Web服务器运行. 而github pages不是一个应用环境, 他只是一个git仓库, 里面只能放静态文件. 所以静态化的思路就是在本地运行博客并请求所有路由, 将请求得到的html响应(包含格式)写入html文件, 并将html文件部署在github上. 这样请求博客路由, github服务器就会返回写有html的静态文件, 这就是其被称为静态化的原因.
静态化也有局限的地方, 那就是无法实现用户登录等动态交互功能. 不过博客本来就是用来看的, 这些局限不是太大的问题.

实现

通过上段的介绍, 我们知道了实现静态生成器的2个关键环节:

  1. 获取所有路由
  2. 模拟请求所有路由, 将响应写入文件

先看看如何获取所有路由吧.
本地博客的所有URL(路由)分为三大类: 第一类是不含参数的确定路由, 有如下这些

这类路由是固定的, 与博客的数量无关.

第二类是需要通过url_for生成完整url的含参数动态路由, 有如下这些

这些路由通过url_for传参生成, 比如通过相对路径访问博客页面的路由:

.post表示生成post视图函数对应的路由, path=post.path就是将post.path的值传给path参数构建完整url.

第三类路由是Flask默认处理静态资源的路由, 不需要用户定义, 可以通过url_for生成:

加载相应路径下的静态资源(static/css/application.css, static/js/application.js).

第二和第三类路由的数量是不确定的, 第一类路由虽然固定, 但是写死似乎不太好看(⊙﹏⊙)b, 那么如何获取所有路由呢? 蛤! Flask的膜法就起作用啦!

所有路由可以通过iter_rules()方法获取:

>>> list(app.url_map.iter_rules())
[<Rule '/archives/' (HEAD, OPTIONS, GET) -> archives>,
 <Rule '/about/' (HEAD, OPTIONS, GET) ->about>,
 <Rule '/tags/' (HEAD, OPTIONS, GET) -> tags>,
 <Rule '/' (HEAD, OPTIONS, GET) -> index>,
 <Rule '/archive/<year>/' (HEAD, OPTIONS, GET) -> archive>,
 <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/tag/<tag>/' (HEAD, OPTIONS, GET) -> tag>,
 <Rule '/<path>/' (HEAD, OPTIONS, GET) -> post>]

这样第一类路由就可以轻松拿到了.
那么动态路由怎么处理呢? 对于动态路由肯定不能指望一个方法返回所有路由, 因为动态路由只有在请求传入参数的时候才能确定, 所以如果能设置一个url_for钩子, 在每次调用url_for时将url_for生成的路由存储起来就好了. Flask就提供了这样一个接口: url processors

url processors can automatically inject values into a call for url_for() automatically.

我们可以实现这样一个钩子, 在第一次调用url_for时将动态生成的url存储到双端队列self.calls中:

这样我们就可以拿到所有的动态路由啦! 至于call为什么用collections.deque实现, 因为这个类型是线程安全的.

接下来就是发模拟请求并将响应写入文件, 可以用Flask的app.test_client()发送请求. 顾名思义, 这个API本身是用于测试的, 这里正好可以用.

说说这里为什么用了yield. 静态博客生成器速度是关键, 一些爱写博客的人可能有几百篇博客, 对每个博客进行请求并等待响应, 这个过程中的网络IO是非常耗时的. 为了提升速度, 我一开始想到的是用协程进行处理, 然而可惜, Flask是线程模型, gen_client.get不是coroutine... 所以这里的yield其实并没有什么卵用... 至于多线程, GIL... 一个生成器开多进程又有点小题大做了. 测了一下生成速度, 200篇文章5s(包含外部图片请求), 可以接受, 那就这样吧.(ps: 等我写到200篇会优化的O(∩_∩)O)
将响应写入文件很简单, 直接上代码:

综上, 生成器程序的核心结构是两个类: Gen类UrlForGen类, Gen类用于处理第一类路由, 包含一个入口方法(gen). UrlForGen是一个上下文管理器类, with该类对象, 插入钩子获取动态路由, 传递给gen处理. 最后gen方法模拟请求所有路由, 并将响应写入文件存入相应目录. 完成静态化!

实现命令行接口

OK! 现在我们已经完成了本地博客和静态化模块. 接下来就要码最终呈现给用户的命令行接口啦.
我选择了click框架来实现命令行接口. click使用起来很简单

接下来我们一个命令一个命令的看.

railgun init

railgun init的作用是创建用户本地博客. 也就是想办法把我们搭建的本地博客模块拷贝到用户指定的路径处. 我们知道一开始用户安装railgun的时候会安装相应的代码到命令目录下, 如果我们把本地博客打包连同命令一起上传, 用户安装后就会在自己的电脑上拥有一个本地博客副本了! 而且我们不用管用户把railgun命令安装在/bin//usr/bin/还是/usr/local/bin/, 我们只需要使用相对路径定位本地博客目录, 就可以找到它了!

railgun new

railgun new用于新建一篇博客,

用户执行railgun命令时的目录是本地博客的根目录, 顺着相对路径就可以找到存放博客的pages目录了, 然后新建一篇博客.

railgun server

railgun server会启动预览服务器, 这里直接用os.popen()调用python manage.py runserver就可以了,

railgun build

railgun build用于静态化博客. 考虑到我们的静态化模块需要依赖Flask API(iter_rules, url processors)以及用户配置(GEN_BASE_URL)工作, 所以我把静态化模块的启动放到了本地博客的manage.py中:

然后railgun build调用os.popen('python manage.py build')执行静态化工作.

railgun upload

railgun upload根据用户配置将静态博客部署到指定git仓库上.

railgun upgrade

主题系统

接下来