耗时三天用Go编写博客系统
之前在谈玩博客的历史的时候简单讲述过我开始自己写博客系统的心路历程:作为极简主义者,我不能忍受有多余的我用不上的功能,所以我选择完全根据我的喜恶打造一个纯自用的博客系统。出于分享,我把我的心路历程放在这里。
构思
首先我思考,我想要一个什么样的博客?我希望实现什么功能?我打开了一个文档,记下我的想法。这个阶段我不思考任何关于实现的问题,只是列下我想要的、我的愿望,记录我想象中的博客是怎么个样。
我很喜欢notion和grav的嵌套系统,很有条理,所以我要保留它们。因为用了很长时间的grav,我可以参考grav的结构。不过,我并不完全喜欢grav,所以我才要更换。我不喜欢哪里呢?是文件结构太复杂了。每个文章外面都要套个文件夹,我不喜欢那样。所以我要根据自己的审美简化它,把它改成适合我的。
我在用grav的时候主要就用两个template类型,一个是blog,另一个是Item。Grav之所以要设计文件夹包裹,是为了方便路径名和模板名的声明。但是只要我们把结构固定,就不需要每次查询模板名字了。
因此,我想出了如下的系统:
在一个文件夹中,必定有一个_index.md,我叫它list,它实际上是grav的blog.md,负责储存这个列表的信息;剩下的md文件都是post,也就是grav中的Item,它们的文件名是路由链接。这样牺牲了调用模板的自由,但是因为我没有创建画廊啊之类的其它template的打算,所以这对我来说算优化——个人的取舍。
不过,我仍然应该保留grav的default,我在这里把它命名为了page,规定所有放在根目录的md文件类型都为page,这样我的首页就不会被post的模板影响,也可以临时建造一些别的页面。对于一部分让人来说,这是一个限制很大、不够自由的系统,然而对我来说却正正好好。
还有,附加的,我很喜欢grav通过01、02这样的文件夹数字前缀进行排序,我想继承这个系统。我也希望继续用过markdown的frontmatter储存必要信息,我现在需要的是标题、日期、是否上锁,因为我的草稿写在本地电脑上,我去掉了是否可见。
说到上锁,我觉得博客搞账号密码系统太复杂了,我其实甚至不想实现评论功能,于是我决定用页面上锁和access code来代替。不输入正确的暗号就什么都看不到,简单粗暴。
建立结构
之后,我简单熟悉了一下博客系统的结构。我个人觉得结构是最重要的一环,如果想不清结构,就写不出逻辑清晰、职责明确的代码。我向GPT师傅请教:如果用Go写一个博客系统,结构应该是怎么样的?
师傅告诉我,大概是这样的:
-
router
: 负责路由注册 -
handlers
: 负责业务逻辑处理 -
render
: 负责 HTML 渲染
除此以外,额外的功能在utils。
功能列表
有了必要的信息,我开始思考我的功能实现,并按照它们的重要程度排序,方便我从最小化开始做。根据我的进度,我还可以继续添加想要的功能。当我完成了一个任务后,我就划掉它以增加成就感。
这是我的第一版列表:
- 从markdown到html自动渲染
- 路由系统
- 数字前缀系统
- list列表
- 好看的css
- 优化(其他功能都完成之后,最后考虑优化,理论上这个永远处于最低优先度)
实现基础
有了这样的指示之后,我开始在ChatGPT的帮助下学习并编写Go代码。因为有gin和goldmark这样方便的库,我的任务1、2很快就完成了。
路由说真的顶多就花了五分钟的时间,不过渲染让我有些惆怅。md到html的渲染是很简单的,但唯一的问题就是我不会写前端,不知道怎么该动态渲染。于是我使用了go的html/template,把渲染好的文章塞进去。虽然它有一些复杂的嵌套机制,但得益于同样是模板,我可以简单修改后重新利用我的Twig模板。
到了数字前缀系统,我有一些头疼。我想和grav一样,在URL中自动删除数字前缀,让链接更美观。从文件路径匹配到URL是简单,但反过来就很难了,每一次都要遍历文件来匹配,我觉得这样很浪费io。为了解决这个痛点,我增加了一个新的必须实现的功能:缓存。
我建立了一个URL到文件路径的映射表,在程序开始的时候初始化它。此外为了防止改名、删除、移动位置等情况,我还设计了一个Rebuild function,从链接的最后开始重新匹配,详情就不说了。
总之这是我缓存的第一步。
之后,我写到list,我意识到每次打开list页面都要遍历一遍文件夹,读取全部内容,再拆分frontmatter和正文,这是非常没有效率的。因此我在这里建了第二个缓存,我储存了每个文章的链接、标题、时间、修改时间、文章总结(实际是读取了前1kb内容),每次渲染list的时候检测一遍文件是否存在,以及通过对比修改时间来检测有无修改,比反复读取和解析yaml节省很多性能。
实现附加内容
基础的内容完成了,经过debug和一切杂七杂八的优化以后,我开始慢慢加上附加的内容,包括不限于:
- img的attribute支持(goldmark已经不支持这个,需要手动正则转换)
- list自动分页优化渲染
- 搜索
- 相邻的文章推荐
- 最新文章
搜索让我花费了一段时间思考,我不喜欢每次搜索都遍历所有文件,因为一旦文章数上来的,速度肯定会慢得像乌龟爬,用Google搜索又感觉有点不友善和掉价。最后我决定复用缓存里的那1kb总结,虽然搜索结果稍微不那么精准,但速度快而且勉强能用。
然后是最新文章,因为有缓存,比较好说,不过我把它置于首页,而且我首页各种元素还挺多,我有点担忧它会拖累加载速度。为了避免那种情况,我先是让最新文章的模板渲染一段占位符,然后由js检测,调用api(这个api我设为了只有127.0.0.1或者::1可以访问),然后再替换。我真的思考了很多类似的小优化。
完成这些之后,我又想,好不容易搞了个动态渲染的博客,却一点互动都没有,太浪费了吧。我不想加评论,因为评论真的不好维护而且我懒得读;加浏览数呢,又想公开处刑一样很尴尬。最后我决定折中,加一个star功能。我本来想叫点赞的,但我又想,我都学计算机的了,怎么能不致敬一波github?于是就用了star。之后还顺手加了个复制链接分享。
不管怎样,我注册了两个api,一个获取赞数,另一个增加赞数。为了减小储存占用,也为了防止我产生邪念,我把所有的赞数和用于去重的已赞ip列表储存在了二进制文件中,每次程序启动的时候读取,之后只写不读减少io。
值得一提的是,这里面有些功能其实是我在上线之后再增加的。三天的时间里我只写了基础内容和几个附加项。不过啊,仔细看过我博客首页的可能会发现,我实际上是花了四天才上线的,那多的一天我在干嘛?
——CSS折磨了我一整天。
编程不懂可以查、可以写,但如果审美没救,那网页设计就完全没救了。这里不建议和我一样想不出来硬想,觉得丑但是不会改然后硬改,多看看别人的博客和网站获取一下灵感更好;自己写不出原创就上网抄一段。反正不建议没苦硬吃,快给我写出心理创伤了。
总之,我花费了3天时间,用Go重写了我的博客系统。它绝对算不上完美,仍然有不少潜在的bug和优化等待我发掘,不过我喜欢它胜过任何一个我用过的博客系统——因为它是我自己写的。