自己动手写动态博客

# 缘起

2019年,我写了一个动态博客,并发表了一篇博客《[自己动手写动态博客](https://hpdell.github.io/%E7%BC%96%E7%A8%8B/dynamic-blog/)》,主要介绍了基于 Quasar 和 Express 的动态博客框架。但是后面这个动态博客被废弃了,原因是多方面的。一方面项目的部署没有基于 Docker,导致维护起来比较复杂,经常发生无法访问的情况。另一方面,该项目是前后端分离的,文章内容通过接口进行获取,再加上没有 SSR,所以就算想要提交搜索引擎,也很难进行搜索。此外,项目缺乏一个高可用性的后端管理平台。再加上当时网页设计水平有限,一些设计比较反人类,因此最终还是继续使用了基于 GitHub Pages 的静态博客。

其实在做这个动态博客之前,并没有真的打算做一个博客出来,只是为了试一下 [django-vditor](https://pypi.org/project/django-vditor/) 这个包,甚至项目文件夹名称都是 `test-django-vditor`,因为 [Vditor](https://b3log.org/vditor/) 差不多已经是前端最强 Markdown 编辑器了。倒是初衷和博客有一定关系。2020年初设计了 GWmodel Lab 的[主页](http://gwmodel.whu.edu.cn),内容很全。由于网页是部署在群晖的 Web 服务器上,限制很大,所以项目虽然是前后端分离,但是总体上是一个静态网页,所有的 API 全都保存成了 JSON 文件。但是维护起来非常麻烦。这个主页一共分为了三个仓库:React 前端、Markdown 内容、Django 管理平台。当需要增加内容时,先在本地写好 Markdown 内容,使用 Git 进行版本管理,然后在 Django 中向数据库中添加相应的记录,再将接口响应内容保存成 JSON 文件,最后将 React 前端和内容以及接口相应内容部署到 Web 服务器上。当我还在实验室的时候,整个流程已经被打通,更新还算方便。但是交接给师弟的时候,想要讲清楚整个流程,就非常难了。

那么有没有一种比较方便的管理方法呢?有,其实只要找到一种方法,让 Django 直接可以输出整个网页,然后使用一些脚本将这些网页保存成 HTML 文件,再发布到网页服务器上就可以了。这个过程甚至可以让 Django 自己完成。配合 Django 的管理平台,更新内容就不复杂了。进一步地,如果要对搜索引擎友好,可以抛弃前后端分离的思路,反而使用 Django 的模板引擎渲染页面。

基于以上思路,我做了这个动态博客,就当作实验室主页的一个小 Demo。

# 框架

这个动态博客的框架非常简单,就是 Django + Bootstrap,没有 MVVM 框架,没有前后端分离,以至于写的时候仿佛回到了自己 2016 年用 Bootstrap 写网页的时代。

但是这个框架对于个人博客来讲,就显得很方便了。毕竟个人博客的项目复杂度不高,没有使用前端框架的必要,也没有前后端分离的必要,事实上,前后端分离反而会带来很多其他麻烦。而且 Django 现在的功能已经非常强大了,很多功能(例如图片上传、权限管理)只需要增加一些配置就可以解决,其管理平台更是可以让我们少些很多代码。经过几年的发展,Bootstrap 使用起来也很方便了,而且样式并不过时,也很简洁,很适合个人博客。

下面介绍一些具体的细节。

## 数据库

似乎每一个教关系数据库的教程,都会用博客作为案例场景进行介绍。这是因为这个场景非常简单,但是涉及了一对多、多对多两种关系。通常,会有四张表:文章(Post)、分类(Category)、标签(Tag)和作者(Author)。本框架暂时省略了作者表,因为目前来说作者只有我一个人。数据库中主要的表和它们的关系如下图所示。

```mermaid
erDiagram
    Post }o--|| Category : "belongs"
    Post }o--o{ Tag : "has"
    User ||--|| Profile : "has"
```

其中几个类的具体情况是

```mermaid
classDiagram
  direction RL
  class Post {
    title : CharField
    content : TextField
    date : DateField
    cover : ImageField
    category : ForeignKeyField[Category]
    tags : ManyToManyField[Tag]
  }
  class Category {
    name : CharField
  }
  class Tag {
    name : CharField
  }
  class Profile {
    user: OneToOneField[User]
    avatar: ImageField
    content: TextField
  }
```

可以说这真的是最精简版的博客数据库了。当然数据库中实际存在的还是有 Django 自带的权限表 Group 和 User。为了使用 Vditor,实际实现时还是要把 TextField 替换成 VditorTextField,才能在 Django Admin 中使用 Vditor 进行编辑。

## 视图和模板

视图这部分没什么好说的,主要是以下几个视图:

- Post 的增删查改
- User 的登入登出
- Home

登入登出功能主要是限制 Post 增删改的权限。但是如果不要求在前端进行文章操作,完全可以不要登入登出和 Post 的增删改操作(比较适合导出成静态页面)。

主页 Home 的渲染是根据 Profile 中关联的第一个 User 表中用户的记录。由于这个项目在使用 Docker 部署的时候,必然会创建一个超级用户,这个超级用户就是作为第一个用户存在的。Home 中最左侧的介绍以及中间的头像,就是从数据库中取出这个超级用户的相应记录进行渲染的。

模板几乎与视图一一对应,只不过为了避免写重复的代码,将模板进行了分解,使用 Django 提供的 `extends` 关键词进行模板扩展。主要继承关系如下所示。

```mermaid
classDiagram
  base <|-- index
  base <|-- post_base
  post_base <|-- list
  post_base <|-- detail
  post_base <|-- edit
```

日后还可以在此基础上添加其他的模块,例如相册等。

# 部署

该项目提供了 Dockerfile 用于构建 [Docker 镜像](https://hub.docker.com/r/hpdell/myzone-django),可以直接使用 Docker 部署。事实上,该项目就是利用 VSCode 的远程开发功能在 Docker 容器里面开发的。同时仓库中也提供了一个 docker-compose 配置样例,可以直接部署。下图是在服务器上部署后的效果。

![image.png](/media/870963a7-42f5-4d62-81c2-8876b1949943_image.png)

相应的 docker-compose 配置如下:

```yml
version: '3.8'

services:
  app:
    image: hpdell/myzone-django:latest
    volumes:
      - upload-data:/code/uploads
    ports:
      - "8080:8000"
    depends_on:
      - db
    links:
      - "db:db"
    restart: unless-stopped
    environment:
      - DJANGO_SUPERUSER_USERNAME=
      - DJANGO_SUPERUSER_EMAIL=
      - DJANGO_SUPERUSER_PASSWORD=
      - MYZONE_HOST=huyg.site

  db:
    image: postgres:latest
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER:
      POSTGRES_DB:
      POSTGRES_PASSWORD:

volumes:
  postgres-data: null
  upload-data: null

```

注意配置中将 `uploads` 文件夹(上传的图片所保存的位置)永久性保留了下来,这样每次代码更新,直接拉取最新镜像并重新部署容器即可(在 Protainer 中甚至只需要点点鼠标)。

# 结语

整个博客开发时间,满打满算可能有四天,就已经实现了绝大多数功能(有一些在文章中没有介绍,比如本地化、移动端适配),相比之前那个动态博客,开发难度降低了很多,而且可扩展性也很强。这得益于 Django 的强大功能,以及最简化的项目框架,这样我觉得没有选择最火热的 Vue/React 和前后端分离框架是一个非常正确的决定。这样我想起一段往事。

> 去年我在进行毕设答辩的时候,有一个评委老师问我这样一个问题:“你觉得模型是越复杂越好,还是越简单越好?”我给出的回答是:“模型的复杂度要与实际问题相匹配。”

虽然我不知道这个回答有没有让老师满意,但在开发做项目时,我觉得也是同样的情况,用的技术栈也不是越强大越好,而是要和项目内容相匹配。用一些简化的技术去开发复杂的项目,难度一定会越来越大;而用一些太过强大的技术开发简单的项目,反而会带来很多不必要的麻烦。但是,这并不是说 Django 模板系统不强大,只是基于模板的渲染系统在项目规模更大时会变得比较麻烦,此时采用前后端分离的框架会更好。

当然,这个博客还有一些功能有待完善:

* [ ]  草稿功能。思路是在 Post 中增加一个 Draft 字段来表示是不是草稿,前端再增加一个草稿箱的列表页面。
* [ ]  自动保存。思路是借助 Vditor 自带的缓存功能实现。
* [ ]  退出提醒。退出编辑页面时提示用户数据可能没有保存,避免文稿丢失。

希望这个博客能够稳定运行更长的时间。