从零搭建博客

July 30, 2020

技术栈:

React (前端框架)

Python (后端API)

Nginx (WEB服务器,链接前端与WSGI服务器)

uWSGI (网关,链接服务器与后端)

Docker (容器化)

Google Cloud Platform (云端服务器)

Kubernetes (管理容器化的app,负载,和服务;自动化部署容器)

Google Domains (购买域名)

前言

在把这个网站真正搭起来之前,我也有过专门写博客的网站,但是构建的过程几乎没有涉及到任何开发方面的知识。源代码用的是hexo框架,服务器用的是github为每个账号都提供的 github.io,在godaddy上买了个域名,看着别人写的教程照猫画虎就搭起来了。用起来没有什么问题,自己调整了一些配置之后也比较满意,但是毕竟不能算自己写的网站,也无法知道开发过程中的各种细节和原理。于是借着这次实习的空隙,花了几周时间认真研究了一下,重写了所有代码和服务,总算是真正有了自己的网站。写这篇文章也是想根据折腾的过程来总结和简化一下开发的过程。如果有想做类似事情的同学看到这篇文章,也能少走一些弯路。虽然涉及了不少技术,但是我也只是略微学懂了一些表面上的东西,想必过程还有很多可以优化的地方,感谢各位提出意见。

结构

在写细节之前,有必要对整个网站的结构做一下描述。

任何网站都分为前端和后端,前端决定了用户看到的界面是什么样子的,后端则是为前端提供需要渲染出来的信息。拿kefan.me举例,网站打开之后,前端需要知道我写了哪些文章,于是向后端发送一个请求。后端接到这个请求之后开始处理,然后再把博客的列表返回前端,这时候前端知道了这些信息,于是把他们渲染到特定的位置。这也是典型的前后端分离的逻辑,也是比较符合直觉的一种设计。前端只负责网站看起来是怎么样的,后端提供的也只有一个api的接口,作为处理请求和数据的工具人。当然在具体操作的时候,在前后端中间还要添加两个中间层,一个是nginx,一个是uWSGI。后者把api运行在一个端口上,而前者则监听来自前端的请求,如果有朝后端发射的请求,则把请求的内容发送到后端的端口上。目前这两个过程听起来还很抽象,等实际进行到那一步的时候会发现这些操作还算是比较直观的。

在这些步骤都完成之后,这个网站已经可以在本地(localhost)运行起来了,下一步就是把它部署到一个服务器上。这里我选择的是Google Cloud Platform (GCP),服务器的选择并没有太大的区别,目前主流的几家平台(Azure, AWS, GCP)都可以提供足够的服务(毕竟这个博客也没有多大的流量),选择谷歌只是因为价格比较便宜,交互的界面看着也比较顺眼。当进行到这一步的时候,在任何设备的浏览器上输入谷歌提供的ip就可以看到网站了(格式大概为 http://xx.xx.xxx.xxx )。最后一步要做的就是把这个ip地址和购买的域名连接起来。对于我来说前端和后端写起来都比较轻松,虽然代码数量比较多,但是起码知道怎么写,而且任何问题都可以在网上搜到解决的办法;真正难到痛苦的是后面的几个步骤,因为根本没有找到一个足够详细的教程来解决我的问题,遇到不懂的只能根据log里面的错误信息去官方的文档里找,来回反复改了几十次,总算才把问题都解决了。希望这篇文章可以把整个过程讲清楚。

准备工作

整个网站的开发是在MacOS上进行的,所以用linux的同学估计不用有太多担心。用Windows的话建议装一下WSL2,这样就可以避免很多兼容性的问题。毕竟微软的开发环境实在是比较一言难尽。

一些需要提前准备的东西:

prerequisite
├── nodejs
│   ├── npm
│   └── npx
├── python37
│   ├── pip3
│   └── virtualenv
├── docker
│   └── docker desktop
├── nginx
└── k8s stuff
    ├── google account
    ├── gcloud cli tool
    └── kubectl

这些东西也不必提前装好,大可以在部署过程中需要的时候再去安装,写在这里只是为了看的时候清楚一点。

另外域名也是一个需要考虑的部分,每个网址的价格都不一样,比如 ke.fan 这个短小酷炫的域名一年的租金就要600+CAD,实在是买不起。而目前使用的 kefan.me 每年的租金也只有30刀不到。关于购买域名的网站我比较推荐 Google Domain 和 GoDaddy。都是大网站,客服比较到位。

另外值得一提的是如果要注册国内的域名的话需要去指定的管理局备案,这个过程大概要花费几周,更多的细节就不清楚了。

Now let’s get started.

前端(React)

首先我们用react官方提供的脚手架创建一个项目,名字就叫做mywebsite(可能会需要几分钟)

$ mkdir mywebsite
$ npm uninstall -g create-react-app mywebsite #remove oudated version globally
$ npx create-react-app mywebsite --use-npm

完成之后一个粗糙的项目结构就出来了,拥有,输入

$ cd mywebsite
$ npm start
$

Compiled successfully!

You can now view mywebsite in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.2.xx:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

就可以在本地的 localhost:3000 上看到初始的网站了。现在的网站上只有一个react提供的默认介绍页面,静态内容由 src/index.js 提供。

现在的代码结构如下:

mywebsite
├── README.md
├── node_modules/
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js

3 directories, 18 files

在这之中,node_modules文件夹里有node所需要的插件包(很多);package.json里是react project的信息与介绍;public里是一些静态网站所需要的文件,比如缩略图favicon和方便爬虫爬取的robots.txt。而src里面则是react的源代码,对于前端来说所有的代码都会被放在这里。

至此,前端的基本设置就基本完成了。

三:后端(Flask + uWSGI)

前端做好了,我们现在需要一个处理数据的后端。这里的选择是用python写的flask。

新建一个app文件夹,再创建一个app.py:

import os
from flask import Flask, request, jsonify
from flask_cors import CORS
from frontmatter import Frontmatter
from gevent.pywsgi import WSGIServer

app = Flask(__name__)
CORS(app)

@app.route('/health')
def health():
  return '', 200

@app.route('/ready')
def ready():
  return 'Success', 200

if __name__ == "__main__":
  WSGIServer(('0.0.0.0', 5000), app).serve_forever()

在这段代码中,flask的端口被开在5000上面,同时定义了几个测试用的链接。为了让这段代码成功地跑起来,我们首先列出一份python所需要的列表:

Flask==1.1.2
Flask-Cors==3.0.8
frontmatter==3.0.7
gevent==20.6.2
gunicorn==20.0.4
greenlet==0.4.16
requests==2.25.1

接着我们需要为python建立一个virtual enviornment,这样我们就不用把这个网站所需要的插件永久性地安装在电脑中:

$ pip3 install --upgrade pip
$ python3 -m pip install --user virtualenv #install virtualenv
$ python3 -m venv venv #create env named 'venv'
$ source venv/bin/activate
(venv) $ sudo python3 -m pip install --upgrade pip
(venv) $ pip3 install -r requirements.txt
(venv) $ python3 app/app.py

# when exiting:
(venv) $ deactivate
$ #no longer in virtualenv anymore

在venv中我们安装了所有flask所需要的python包,这样以后每次我们在本地运行flask的时候,我们只要进入这个虚拟环境再运行app.py就可以了,而不需要每次都用pip把requirements里面的包都安装一遍。如果需要删除这个环境,唯一需要做的也只是删除venv这个文件夹。这就使整个开发过程简单了许多。

在virtualenv中运行了刚刚写好的app.py之后,我们打开浏览器输入 localhost:5000/ready 就能看到刚刚程序中在’ready’下写好的’Success’了。

在这里,我们已经:

  • 用react把前端开在3000端口上,负责渲染后端提供的信息
  • 用flask把后端开在5000端口上,负责提供信息给前端

显而易见,对于本地开发来说,如今唯一需要做的就是把这两端连接起来。换句话说,我们需要从前端(3000)把位于后端(5000)的信息抓取过来。

用axios链接前端与后端

对于这种需求,或许更直白的解释方法是提供一个实际开发过程中所遇到的例子。

在下面的代码块中,我用了两个hook来负责更新数据(loading & content),而id是从上端传入的输入参数。在react中,异步请求数据常用的包括jquery,ajax,和axios,而这里用到的则是后者。

我们用axios向localhost:5000 post一个请求,再把收到的回复存入变量。

import React, { useState, useEffect } from "react"
import axios from "axios"

const example = (props) => {
  const id = props.match.params.id
  const [loading, setLoading] = useState(true)
  const [content, setContent] = useState({})

  useEffect(() => {
    setLoading(true)
    axios
      .post("localhost:5000/api/get_post", {
        id: id,
      })
      .then((res) => {
        setContent(res.data)
      })
      .catch((err) => {
        message.error("Error ", err)
      })
      .then(() => {
        setLoading(false)
      })
  }, [id])

  // ...
}

其中后端部分的代码如下:

@app.route('/api/get_post', methods=['GET', 'POST'])
def get_post():
  return jsonify(some_data), 200

尾声

至此开发网站所涉及到的步骤基本都结束了。我们有了一个react写成的前端和一个flask写成的后端,分别开在本地的两个不同端口上。经过一些开发工作后,我们就可以在localhost上看到最终部署在自定义域名上,从任意一台联网的设备上所看到的页面了。但是在部署工作开始前,依然有一些细节最好在现在就加以注意,以下是我回顾自己开发的过程中所总结的一个列表:

- [ ] 用 .gitignore 定义哪些文件可以被上传,哪些文件只能留在本地
- [ ] 添加一个开源许可证 (LICENSE)
- [ ] 做好代码规范,比如用一个第三方的formatter来自动审阅格式
- [ ] 在requirement.txt和package.json中删除多余的dependencies
- [ ] 写README(如果要上传到github的话)

当本地开发完成以后,整个project的树形结构图大概如下:

mywebsite
├── LICENSE
├── README.md
├── node_modules/
├── app
│   └── app.py
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
├── requirements.txt
├── .gitignore
├── src
│   ├── app/
│   └── index.js
└── venv
    ├── bin
    ├── include
    ├── lib
    └── pyvenv.cfg

关于搭建博客的第一部分就先写到这里,本来也不算长的文章愣是来来回回拖了半年多,一路从东方的七夕写到了西方情人节。其中大部分时间都在忙其他的东西,只有偶尔打开文章列表的时候才会感到内疚。作为少有的技术方面的东西,我肯定希望能尽量涵盖所有的细节和要点,但是作为一名大二学生,在中文写作能力已经大幅退化的今天,写作过程中也难免会有疏忽和遗漏。如果在以上的内容中有措辞不当,行文混乱的情况请大家谅解。

希望各位在疫情期间保持健康,如果有第二部分的话,我们第二部分再见。