Maeiee Weekly No.12:我的博客技术架构
前言
新博客(maxieewong.com)上线已经半年了。从静态生成器到前端界面,全是我从头开发的。再本期中我打算对其 Review 一番,总结一下博客的技术架构,以及背后的思考。
本周在目标设定上有点激进,不断叠加 Flag,导致差点翻车,好在最后给兜住了。
差点翻车的目标
原本想的很简单,因为更换主力机,把静态博客生成器迁移过来,顺带梳理一下。
结果呢?
- 不太满意代码实现,重构一下 Python 代码
- 本周想学 Rust,那就用 Rust 来重构吧(🧠:你会吗?
- 本周完成 80%
- 我要 100% 用 Emacs 开发!(VS Code:……
值得一提的是,由于 WSLg 不稳定,Emacs GUI 版本经常会卡死。因此我使用的是命令行版本,经过一个礼拜的开发,不仅 100% Emacs 开发做到了,连鼠标都不需要了……
静态博客生成器目前已经重构完成 80% 了,中秋节应该就能够用上。
博客特色介绍
maxieewong.com 没有使用现成的静态博客生成器。首要原因是我使用 MediaWiki 做知识管理,希望由 MediaWiki 生成博客,这个需求有些小众。第二点是我对博客融入了一些自己的思考,因此独具特色。下面我先介绍下这些特色。
注:博客生成器不开源,原因是我的精力有限,没有精力去维护一个完整的开源项目。同时采用这套知识管理栈的人很少,即便开源,通用性也有限。
基于 MediaWiki 的知识库
我使用 MediaWiki 作为知识管理工具。MediaWiki 是维基百科的基石,支撑了世界上最大的知识库,在功能上也是强大无比的。这也是我选择它的原因。
目前我的 MediaWiki 知识库中包含 2793 篇笔记。本站中的所有文章,也来自于其中,只是其中的一小部分。
静态站点生成器
maxieewong.com 是一个纯静态站点,我写了一个静态博客生成器,能够将 MediaWiki 页面导出,最终生成一个静态站。
静态站没有使用 React/Vue 这些框架,而是使用纯静态的 HTML,能简化就简化。
整个过程如下图所示:
ChangeLog
鲁迅说:文章与其说是写出来的,不如说是改出来的。我深感认同。
文章的第一稿通常比较简陋,需要进行反复修改、打磨。有的优秀作者非常由耐心,能够长期酝酿一篇好文章。而我属于专注力涣散型的,没法长期专注一个事情(这也是为什么我通过 Weekly 强迫自己专注)。
因此我借鉴“迭代开发”的思路,“快速上线,小步快跑”。我会在文章不成熟时就及早发布,随着理解不断加深不断进行完善。
这带来一个问题,用户怎么知道文章更新了呢?
为此我增加了一个 ChangeLog 的实体。对应于博客的首页时间轴。
首页时间轴记录了我对文章的修改历史,就像 Git Commit Log 一般。ChangeLog 的机制也鼓励我不断回顾已有知识,从而帮助我“深入”下去,防止自己浅尝辄止。
注:有一个待改进点,同一天的多次提交其实可以合并。图中是未合并情况,容易造成刷屏。
分类页
像大部分博客一样,maxieewong.com 也有分类功能,通过侧边栏进入。
与众不同的是:分类页自身是一篇文章。
分类页是一篇文章的好处是什么?好处是可以帮助我搭建知识体系。
注:这是 MediaWiki 的一个特性,每个分类页自身也是一个页面。
举一个具体案例:当我在研究 Flutter 引擎的时候,由于规模过于庞大,容易只见树木不见森林。在 Flutter 的分类页中,我得以回到全局视角,用我自己的话来总结整个 Flutter 技术体系。这会帮助我避免钻牛角尖,从而更好进行整体把握。
VisualEditor
MediaWiki 搭载了一个可视化编辑器,叫做 VisualEditor 。
这个编辑器相当的强大,如下图:
比如图中演示了 VisualEditor 的表格编辑能力,能够轻易地合并单元格,并且单元格内还能支持完整的 wikitext 语法(比如嵌套表格)。这种强大的表格能力,是其它编辑器都难以媲美的。
VisualEditor 非常好用,放在一众云笔记、笔记软件里都是非常出众的。
博客架构
博客的架构图如下:
其中主要分为三层。
最底层首先是 MediaWiki 知识库,里面包含笔记,一部分是私有的,一部分是可导出的。除此之外,还有一个目录保存了博客页面的 HTML 模板。
中间层是 Wiki 的 API,除了 MediaWiki API 外。还使用了 Parsoid 服务,它是 MediaWiki 的一部分,用于将 wikitext 渲染成 HTML。
最上层是我开发的博客生成器,重点将在下面小节中介绍。
博客生成器实现
模块划分
主要分为以下模块:
- ChangeLog:维护博客改动历史,并用它生成首页时间线和 RSS Feed
- 页面抓取:访问底层服务,获取页面元信息和页面 HTML 快照
- 拿到 HTML 之后,需要进行一些 HTML 清洗操作
- 页面缓存:将拿到的元信息、HTML、图片保存到本地
- 增量生成:因为有 ChangeLog 改动信息,实现了一个增量生成算法
增量生成
因为有 ChangeLog 改动信息,我能够知道有那些内容改变了。
因此在进行生成的时候,不需要重新拉取所有数据,只需要生成较上次之后变化的数据即可,这就是增量生成。
由于是个人博客,我一周也写不出几篇文章,所以每次生成的时候,只需要重新生成这几篇新的或者有修改的。
有了增量算法,博客生成器没有采用多线程技术,包括网页获取、发请求都是单线程阻塞的,大大简化了代码实现。
每次生成的时间都在几秒钟,速度非常快。
以后哪怕有 1k 篇文章,由于是增量生成,还是只会重拉有改动的几篇,所以效率是恒定的。
数据实体
生成器采用的数据实体也比较简单,列举 Rust 版本如下:
// ChangeLog 改动信息
#[derive(Serialize, Deserialize, Debug)]
pub struct RecentChange {
pub comment: String,
pub newlen: i32,
ns: i32,
old_revid: i32,
pub oldlen: i32,
pub pageid: i32,
rcid: i32,
revid: i32,
pub timestamp: String,
pub title: String,
#[serde(rename = "type")]
pub type_field: String
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PageInfo {
pub pageid: i32,
pub title: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct HomeFeed {
#[serde(rename = "type")]
pub type_field: String,
pub title: String,
pub timestamp: String,
pub url: String,
pub comment: String
}
只有 3 个实体:
- RecentChange:标识一条改动,与 MediaWiki API 对齐
- PageInfo:页面元信息的简化版本呢
- HomeFeed:过滤后的改动,用于生成时间线和 RSS Feed
部署
博客生成器执行完成后,会生成一个完整的静态站点。
我使用 AWS S3 进行静态托管。相关资料有很多,这里不再赘述了。
Rust
通过上述梳理,其实这个博客生成器整体很简单。
但这周自己感觉有点疲惫,主要还是被 Rust 和 Emacs 给带沟里去了。
这一节我先聊聊学习 Rust 一周的感受。
想学一门底层语言
我是一个客户端开发,用 Android 写 Java 起步,后来研究前端 JavaScript 技术栈,再后来搞 Flutter 使用 Dart 技术栈,业余时间里喜欢用 Python 写程序。
随着职业发展的深入,我逐渐从业务层向底层发展,大部分精力在研究 JavaScript Engine、Futter Engine 等框架的底层实现,过去几年里看的最多的代码是 C/C++。
但是我的 C/C++ 属于“哑巴编程”,只会看不会写,顶多只是在底层修修补补。
我意识到,为了再底层更好地走下去,我需要掌握一门底层系统语言。
为啥要学 Rust?
Rust 是近年来热门地底层系统语言,它刚诞生地时候我就有所耳闻,但是直到近年来才变得火爆。
选择就是,C/C++ 还是 Rust?其实两者是各有特色的。我对比的维度如下:
语言 | C/C++ | Rust |
---|---|---|
发展 | 历史悠久 | 年轻 |
生态 | 大 | 小 |
包管理 | 复杂 | 简单 |
构建系统 | 复杂 | 简单 |
编译器报错
友好度 |
中 | 高 |
程序运行
编码缺陷量 |
高 | 低 |
语法复杂度 | 高级高 | 高 |
其中,决定我选择 Rust 的关键是【程序运行编码缺陷量】这一条。
尤其是看了 Flutter/Dart Engine 那十几、几十万行并发 C++ 代码,让我觉得我驾驭不住。
并且 C/C++ 过于复杂,各种宏就像各种咒语。导致不同的项目,尽管都是 C/C++,但是又很不一样,理解起来很吃力。
难学的 Rust
Rust 确实难学,尤其是我平时写的都是 Java/JavaScript/Python/Dart 这种,不用跟指针和内存打交道,而且还有 GC 帮助我们回收资源。
可来到 Rust 后,原来没有了 GC,需要我们自己关注数据是放在堆上还是栈上,关注数据的生命周期,防止出现悬垂指针。
比如用 Python 开发博客生成器,之前写代码都是一遍过的,现在需要跟编译器斗争半天。
不过对我来说,这比 C/C++ 好。C/C++ 里的各种指针、智能指针都需要靠自己去维护,由于复杂度高了以后我驾驭不住,因此会失控,架构会发生混乱。
因此,幸好有 Rust 这个老师在,把一些概念给显化(如生命周期),帮助我意识到自己可能犯的问题。将问题尽早暴露,在早期以低成本解决。
Emacs 开发感受
本周是我第一次 100% 使用 Emacs 进行开发。感受总结来说:
如果不是为了情怀,VS Code 就好。我是纯粹为了情怀,也是为了以后互联网考古,主要是为了玩儿。
如果你是学生,或者刚入门的初学者,我的建议是别浪费太多时间在编辑器上,从 VS Code 入门是一个好选择。不折腾编辑器,先把工作技能修炼到位,挣大钱。
话说回来,在命令行下使用 Emacs,有不一样的体验。因为我会用的命令很少,所以很多操作比较蹩脚,效率上比用 VS Code + 用鼠标要低一点。但是 VS Code + 用鼠标的效率是固定的,而 Emacs 会随着我掌握地加深,效率越来越高,长期回报是可观的。
注:还是需要说明,编码的效率不在编辑器上,就跟写文章的水平不在于写字速度上一样。如果一个复杂的程序不会写,编辑器用的再溜也还是不会写。不要被我带跑偏。
博客未来的架构
博客是我学习系统中的一环,是与我的其它工作流相结合的。
在我的整体系统当中,未来会补齐信息系统,对于笔记系统我再酝酿更换 MediaWiki。
在这些背景之下,博客作为一个微服务,该如何设计呢?
实体完善
现在的的几个实体,RecentChange、HomeFeed 抽象地还是不够好,需要进行更加完善地抽象。
未来除了页面笔记之外,我还考虑打造一个 Info 索引系统,把好的文章、库作为静态站发布出来。
因此,从实体上要有机融合。
更换渲染方式
Parsoid 是 MediaWiki 中的 HTML 快照服务,对我来说整体是一个黑盒,拿到的 HTML 不够纯净。
后续考虑直接拿 wikitext 来自己渲染,比如通过 pandoc。
Lab
我捣鼓的几门技术(Flutter、Rust、JavaScript),都是面向 Web 的。未来会捣鼓一些 Web 小工具什么的。
需要有一个流程化的方式,方便这些 Web 工具的静态发布。
分类
分类是接下来重点关注的内容。
对我来说,每个分类就像一本开源电子书的目录。把纲要写好,能够驱使我体系化地迈进。
下期内容
下周由于临近中秋,事情比较多,就不安排特别的主题了。
把 Rust 博客生成器剩余部分开发完,剩下多读一读文章。
还有就是把老开发机数据备份好。