nifengz

使用Flask构建静态博客

2019-09-12

使用Flask构建静态博客

好久没更新博客了。一直在瞎忙,最近停下来想整理下思路和心情。这让我想起了大学的时候看的《疯狂的程序员》中的话:“学程序,不光要能吃苦能用功,还得看有没有sense。没sense的人就是再怎么学,再怎么给他讲,效果都不好。”,“这就是看三个小时代码和打三个小时游戏的区别,也是有没有sense的关键。看了,你就有sense,没看,你就没有sense。”

近一年大多做一些研究性的项目。于是乎工作变成了面向交友网站编程。搜索,实践,做一些移植和工程化等,大多是python实现。在这个过程中也广泛使用了Flask。Flask作者充分利用了python语言的特性,接口设计十分简洁优雅。所以这次使用Flask作为基础重写了我的博客,这篇文章做个记录。

Why Flask?

我们知道网站一般的架构是:

                         some gateway
         http             protocol  ┌────────────────────────────────────┐
Browser ------> Web Server--------->│ Gateway Server----> Web Application│
                                    └────────────────────────────────────┘

Web Server直接处理来自浏览器(客户端)的请求,主要用于管理连接,路由,服务静态文件,做负载均衡等,常见的如Nginx,apache等。

Web Server一般通过某种网关协议(Fast CGI, PHP-FPM,WSGI等)与后面的Gateway Server(网关服务器)连接,网关服务器可以启动多个Web Application(Web应用)去完成业务逻辑,我们只需实现Web应用,其他的选择合适的开源服务器搭建即可。

在Python Web开发中,使用WSGI作为Web Server 与Gateway Server之间的协议。我选择Werkzeug(完整实现了WSGI协议以及一些有用的工具)作为网关服务器。那么结合上图我们就可以使用 Nginx + Werkzeug + Web App 去构建我们的网站了。再进一步,网站中需要使用模板渲染那么可以使用jinjia2, 一切都是那么自然,Flask 作为胶水,将Werkzeug和jinjia2粘合在一起,加上了路由功能以及扩展框架。这样就构成了python web开发的全部,没有冗余,一切尽在掌握。

静态博客构建思路

好了我们已经清楚网站的架构了,剩下的问题就是构建我们的WebApp了,WebApp应该包含哪些内容呢?在我们这个静态博客场景下,WebApp划分为两个功能:

  1. 构建博客数据
  2. 对上层提供数据服务(根据路由提供不同的数据)

构建博客数据

博文一般都是使用markdown创作的文档,我们需要将markdown文档转化为html格式,并提取其中的信息,比如uri,日期,目录等。我把构建博客的过程称之为build。简单起见,每次Web App启动的时候都会执行一次build,这里我沿用了hexo对博文目录的设置:

posts :博文的根目录
│
├── libco_context_swap :一级子目录,如果这个目录下有md,生成的博客url就是 /libco_context_swap/
│     ├─ libco_context_swap.md :博客文件,生成时会自动转换为html
│     ├─ 1.png :博客引用的图片,在md中直接以 1.png去引用, 生成时会自动生成uri
│     └─ 2.png
│
├── postX
│     └─ postX.md
│
├── _test :以'_'开头的一级目录内的文件和文件夹会原样呈现,并且拷贝后的路径去掉了'_'
│     ├─ test.html :自动生成的uri是 /test/test.html
│ 
└── ...

build的过程就是遍历posts文件夹,按照上面的规则去生成数据。其中markdown到html的转化我使用了markdown2,需要注意的是markdown2默认只提供了基础的格式转换,如果需要额外功能的话需要使用一些扩展。我这里使用了fenced-code-blocks:用于结合Pygments实现代码高亮(或者不采用后端的方案,改用hilightjs在前端进行处理也可以);还有tables:用于显示表格,更多的扩展可以在这里查找。还有一点要提一下markdown2也包含一个用于生成目录的扩展,但是格式不是我想要的,我想使目录包含跳转以及一些我指定的样式,以下代码片段用于生成我的目录:

from bs4 import BeautifulSoup
def _generate_toc(self, soup):
    out_soup = BeautifulSoup("<ol class='toc-nav'></ol>", features='html.parser')
    catalog = ''
    template_str = "<li class='toc-nav-item toc-nav-level{}'>" \
    "<a class='toc-nav-link' href='#{}'>" \
    "<span class='toc-nav-text'>{}</span>" \
    "</a>" \
    "<ol class='toc-nav-child'></ol>" \
    "</li>"
    if soup.h1 is not None:
        for child in soup.children:
            if type(child.name) is str and re.match(r'h[1-6]', child.name):
                h = child.name[1:]
                if h == '1':
                    root_soup = out_soup.ol
                    root_soup.append(BeautifulSoup(template_str.format(h,
                                                                       child.contents[0],
                                                                       child.contents[0]),
                                                   features='html.parser'))
                    else:
                        soups = out_soup.find_all('li', attrs={'class': 'toc-nav-level{}'.format(int(h) - 1)})
                        root_soup = soups[len(soups) - 1].ol
                        root_soup.append(BeautifulSoup(template_str.format(h,
                                                                           child.contents[0],
                                                                           child.contents[0]),
                                                       features='html.parser'))
                        catalog = str(out_soup)
                        return catalog

比如本篇博客,生成的目录为:

<html>
 <head></head>
 <body>
  <ol class="toc-nav">
   <li class="toc-nav-item toc-nav-level1"><a class="toc-nav-link" href="#使用Flask构建静态博客"><span class="toc-nav-text">使用Flask构建静态博客</span></a>
    <ol class="toc-nav-child">
     <li class="toc-nav-item toc-nav-level2"><a class="toc-nav-link" href="#Why Flask?"><span class="toc-nav-text">Why Flask?</span></a>
      <ol class="toc-nav-child"></ol></li>
     <li class="toc-nav-item toc-nav-level2"><a class="toc-nav-link" href="#静态博客构建思路"><span class="toc-nav-text">静态博客构建思路</span></a>
      <ol class="toc-nav-child">
       <li class="toc-nav-item toc-nav-level3"><a class="toc-nav-link" href="#构建博客数据"><span class="toc-nav-text">构建博客数据</span></a>
        <ol class="toc-nav-child"></ol></li>
       <li class="toc-nav-item toc-nav-level3"><a class="toc-nav-link" href="#对上层提供数据服务"><span class="toc-nav-text">对上层提供数据服务</span></a>
        <ol class="toc-nav-child"></ol></li>
      </ol></li>
     <li class="toc-nav-item toc-nav-level2"><a class="toc-nav-link" href="#部署"><span class="toc-nav-text">部署</span></a>
      <ol class="toc-nav-child"></ol></li>
     <li class="toc-nav-item toc-nav-level2"><a class="toc-nav-link" href="#博客快速更新方案"><span class="toc-nav-text">博客快速更新方案</span></a>
      <ol class="toc-nav-child"></ol></li>
    </ol></li>
  </ol>
 </body>
</html>

最终生成的博客数据如下所示:

blog_data = {
    'static_blog_flask': {
        'meta': {
            'title': '使用Flask构建静态博客', 
            'date': '2019-09-12', 
            'tags': ['Flask', '博客'], 
            'uri': '/static_blog_flask/'
        },
        'html': '...', # 这里省略
        'catalog': '...'# 这里省略
    }, 
    'libco_context_swap': {
        'meta': {
            'title': '腾讯libco协程切换分析', 
            'date': '2018-04-12', 
            'tags': ['libco', '协程'],
            'uri': '/libco_context_swap/'}, 
        'html': '...',
        'catalog': '...'
    },
    ...
}

构建完成后生成的数据我们需要保存到硬盘上,以供我们的Web App使用,直接采用python的pickle即可:

import os
import pickle


class DumpUtils(object):

    @staticmethod
    def check_and_get_dict(dump_file: str) -> dict:
        if os.path.exists(dump_file):
            with open(dump_file, 'rb') as f:
                return pickle.load(f)
        return None

    @staticmethod
    def save_dict(src: dict, dump_file: str):
        with open(dump_file, 'wb') as f:
            pickle.dump(src, f, pickle.HIGHEST_PROTOCOL)

好了,这样我们的博客数据已经构建完成并且存储为一个文件了,我们假定这个文件的名字是blog_data.pk,下面我们继续。

对上层提供数据服务

有了博客数据,使用Flask包装一下路由返回数据即可,实现上也没有什么可说的,根据uri读取对应的博客数据模板渲染下即可:

@bp.route('/<path:page_name>/', methods=['GET'])
def render_static(page_name):
    if page_name in blog_data:
        return render_template('post.html',
                               post=blog_data[page_name])
    # 如果不是博客而是其他的静态文件或者html原样返回即可
    root_path = current_app.instance_path + '/' + nifengz_config['posts_data_path'] + '/nifengz/__static/'
    if path.isfile(root_path + page_name):
        return send_from_directory(root_path, page_name)
    abort(404)

部署

前面也介绍了网站的基本架构,所以部署的话我用的是:Nginx作为Web服务器,uWSGI作为网关服务器。这里简单介绍下uWSGI,因为很多人会把这个名词和WSGI, uwsgi混淆。首先我们之前已经介绍过了WSGI,是python web 开发中使用的的网关协议,然后uWSGI是一个和Nginx类似的服务器软件,它实现了多种网关协议,包括WSGI协议,所以我们可以用来当做我们WebApp的网关服务程序,最后uwsgi是uWSGI服务器内部使用的一个协议的名字,这里我们没用到就不展开了,知道它们的关系即可。

具体打包,以及安装我就跳过了,给出一下配置文件的说明, 首先是uWSGI的配置文件:

# nifengz_uwsgi_nginx.ini
[uwsgi]
# 我的WebApp使用工厂函数创建
module = nifengz.run:create_app_wrapper()
# 这个参数可以给你的python传参,我这里就传入了一个配置文件
pyargv=./dev.yml
enable-threads = true

#master = true 因为我使用脚本管理程序的启动,所以就不需要master模式了
processes = 1
threads = 2
# 用于和nginx沟通的socket
socket=/tmp/nifengz.sock
# 设置socket文件模式为可读写
chmod-socket = 666
die-on-term = true

然后 nginx :

location / {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/nifengz.sock;
    }

博客快速更新方案

好了,我的博客部署好了, 现在我写了一篇新的博客,想快速的更新到服务器该怎么做呢? 我是用git来完成自动更新博文的,思路是:将我们的post目录用git管理起来,Post到服务器上,当更新博文后使用git提交项目。使用git的hooks功能, 在服务器上修改post-receive调用脚本完成构建博客数据并重启Web应用。示例流程如下(路径已经匿名):

  1. 在本机初始化我们的posts目录

    # 本机执行
    cd posts
    git init
    # 注意路径,稍后会在服务器上初始化这个git仓库
    git remote add origin ssh://xx@nifengz.com/home/xx/xx_repository/posts.git
    
  2. 在服务器上创建对应的远程仓库

    # 服务器执行
    cd /home/xx
    mkdir xx_repository
    # 创建空仓库
    git init --bare posts.git 
    
  3. 推送本地仓库到远程

    # 本机执行
    cd posts
    git push origin master
    
  4. 推送成功后,我们需要打开 git hooks功能。 在服务器上进入post.git的hooks目录,修改post-receive文件

    # 新建 post-receive 文本, 这里面的脚本将会在收到客户端push后调用
    # post-receive示例
    #!/bin/bash
    log_file=/home/xx/push_update.log
    date > $log_file
    nohup /home/xx/start_in_git_post.sh &>>$log_file &
    
  5. 当客户端提交后,在服务器上启动一个git客户端去拉取这个仓库,然后执行对应的更新任务即可参考start_in_git_post.sh

    #!/bin/sh
    set -e
    cd /home/xx/posts
    sleep 5
    # 在服务器上拉去我们的posts项目
    env -i git pull
    # 下面执行博客构建和重启WebApp即可
    ...
    

以上便是构建整个静态博客的过程。

扫描二维码,分享此文章