← Zakee's Planet

用PocketBase实现网站评论

2023年6月15日 ·

自从换用静态博客系统以来,我的就在折腾评论系统的道路上一直没有停过。这篇文章就简单记录下我折腾博客评论系统的历程以及我最近的折腾成果——使用PocketBase来驱动评论。由于重点只是我自己的折腾记录,所以不要指望能有什么干货就是了。

背景 #

最早用的是Valine1——所谓的“无后端评论系统”,后来事实证明“无后端”并不是一个好主意。自从Valine暴露出一些XSS安全问题和隐私泄露问题后,我就在积极寻找各类替代品。

首先我排除了各种商业服务,投入到了self-hosted的怀抱。不为别的,就是说数据一定要掌握在咱自己手中。这个网站收录了几十个开源自托管的评论系统,我还真的一个一个地过了一遍,有些是本地搭了个试了下,有的只是阅读了下介绍或者文档,然而,完全符合我心意的一个都没有。我的需求很简单,就是要类似WordPress那样的评论,不用登录,任何人都可以发送评论。另外我很喜欢WordPress评论系统有一个“网址”的栏位,评论者可以在这里留下自己博客的网址,方便回访,虽然有时候会有人发广告链接,不过我还是觉得这个利大于弊,遗憾是的很多评论系统没有这个栏位。最后,最重要的一点,一定要简洁轻量,不需要那些花里胡哨的功能,加载一定要快,有些评论系统动辄几百KB的js脚本就离谱。最终比来比去,我觉得isso是最符合我需求的,就把Valine换成了isso,就这样凑合用着了。

其实早多年以前,我把博客从WordPress转到Hugo的时候就萌生了自己做评论系统的念头,可是受限于技术水平,一直都没有实现。曾多次尝试,有的是写了个开头就写不下去了,有的是实现了但又觉得写得太过丑陋及粗糙,没法用。感觉自己不懂的还是太多了,尤其是后端方面。评论系统说简单真的非常简单,说复杂,需要注意的地方确实有很多。

遇见PocketBase #

大概是去年年底的时候看到了一篇博客,很受启发。从博主这里了解到了PocketBase2这个神器。这个简直强无敌,Go写的,编译成单一可执行文件,就实现了数据库CRUD、实时数据订阅、用户管理、文件上传等功能,还有一个颜值很高的管理后台,而更让我震惊的是这个项目竟然是一个人完成的。不得不说作者的审美真的很在线,技术品味也很高。PocketBase使用内嵌的SQLite数据库,对于中小型的应用,性能完全是hold得住的,更不用说我这种没有什么访问量的博客的评论系统了。

不过只有PocketBase本体还不足以作为评论的后端直接与前端交互。由于来自前端的请求默认都不可信任,而且我们要做不用登录的评论系统,这样直接让前端操作数据源风险太大了。PocketBase虽然目前支持设置一些API访问规则,但是现在还不支持把某个字段设置为只有认证通过才返回到响应中。我指的就是“电子邮件地址”这个字段,评论的电邮地址属于评论者的隐私,主要是用来显示Gravatar头像的,前端显示头像只需要电邮地址的md5就行了,原始的邮箱是绝对不能出现在API响应中的。由此可见做一个运行在PockerBase和浏览器之间的服务端非常有必要。而且这个服务端不光起到过滤敏感信息的作用,还可以实现一些其他功能,比如发送电邮通知、Webhook等。

要实现这个服务端,最容易想到的就是各种Serverless云函数了。受到那篇博客的影响,我也很想尝试用Deno来实现。早就听说过Deno的大名,据说是Node.js创始人Ryan Dahl受不了Node.js的种种缺陷重新打造的JavaScript运行时。Deno比较吸引我的地方是项目没有node_modules文件夹,包都通过url的形式引入,非常像在浏览器中引入js脚本那样。一个Deno脚本拷贝到任意主机上,只要安装了Deno环境,就可以直接运行,不像Node那样要还需要一个package.json,还要先npm install。Deno有官方的Serverless平台“Deno Deploy3”,可以非常方便地部署Deno项目。

开始动手 #

我的需求非常简单,就是只需要处理一个GET请求,一个POST请求,一个用于获取某个页面的评论,一个用于创建新评论。所以也用不上什么框架,直接就用Deno的std/http标准库了,对着文档copy,还是很简单的。然后由于PocketBase有js-sdk4,所以与之连接非常简单,直接导入ES模块(也就是SDK中的dist/pocketbase.es.mjs这个文件),然后想要什么操作,对着文档copy就行。至此,前端请求的处理和后端PocketBase的操作都已打通,现在重点来了,handler函数里的“业务逻辑”要怎么写?也就是在这里我才深刻明白我真的很菜。花费我时间最多的就是嵌套评论的实现了。首先我在PocketBase中的创建评论的collection,这个基本上参考了isso的数据表字段,也是为了方便我迁移评论数据过来。然后我查了很多教程,终于是写出来了,无非就是把一个扁平数组转成树形对象,明白了原理其实也没有很难。

到这里,我的服务端就能实现显示评论和发评论的基本功能了,然后就是客户端部分了。本来想直接用原生js来做,毕竟我的网站非常轻量,我不想因为一个评论功能就引入巨大的js脚本。之前我就听说过Svelte这个框架,它是把组件编译成原生js,编译时就实现了Reactive,所以不需要把自己也打包进去,于是当组件不是很多的时候打包的js体积会非常小。在看过了官方教程5之后更是爱上了这个框架,这语法是真的简洁啊,写起来太符合直觉了。Svelte这么舒服,还写什么原生js,MVVM不香吗?基本功能实现后我就想加上Markdown支持,然后我就找到了这个Snarkdown6这个库,能实现基础的Markdown解析,而体积竟然只有3KB大小。然后由于评论会渲染成html,所以要考虑XSS问题,就用了insane7这个库来过滤XSS。insane用的是白名单机制,所以体积非常小,才几KB。最终,我的评论模块脚本打包也只有20多KB,gzip后只有10KB左右。

在实现了基本功能跟之后我就迫不及待地把评论数据迁移到PocketBase。迁移我用了个笨办法,就是把isso的sqlite数据库导出成csv格式,然后用Excel手改😅。因为一共也就170来条,而且基本上需要修改的就是两个地方:一是评论的时间格式,isso存的是unix时间戳,PocketBase要用ISO 8601格式,这个用Excel函数即可,方法网上搜索就有。二是评论id要从连续数字改成10位随机字符串。改好后用DB Browser for SQLite8打开PocketBase对应的表,直接全部复制进去。然后在服务器部署好PocketBase,Deno服务端部署到Deno Deploy,网站文章页面引入打包好的js脚本就能实现评论了。

后面又陆陆续续完善了一些功能,比如Deno这边加了个Webhook通知,我用这个来通知自己有新评论。前端做了一点微小的改进。目前这部分的代码都开源在GitHub上了,我自建的Gitea上也镜像了一份,方便国内访问。这部分的代码请留意需要切换到legacy分支。

你可能会问为什么这部分叫legacy?是的,后面我又重构了后端。


PocketBase可以作为一个Go框架来使用,之前由于我不会Go,所以就没有考虑过这种用法。大概几个月之前,我一时兴起看了下Go的官方教程9,突然对Go很感兴趣。看完了教程的基础部分就开始研究PocketBase用作框架了。主要还是考虑到作为一个简单的评论系统,还要同时运行一个PocketBase实例和一个Deno server,实在是太过麻烦,能把PocketBase和Deno server合在一起就最好不过了。PocketBase的文档在这方面还不够完善,这对于我这种Go新手来说真的非常痛苦。由于PocketBase使用了Echo这个框架,所以很多地方要看Echo的文档,还好Echo的文档有很多例子可供参考,终于东拼西凑把它写出来了。由于之前已经用Deno实现过一遍了,所以用Go重写的时候思路都已经有了,只是要研究下怎么把对应的逻辑用Go语法写出来。

这一百来行的Go代码,我真的写了好久,无论如何,我写出来了,而且它能用,其他什么的都不重要了。虽然很多功能都没有,因为是自己写的,所以有不完善的地方也都会包容,就不像用别人写的评论系统那样挑挑剔剔地了。

目前已经实现的功能有:

如何部署使用 #

目前这个项目还只是自用,暂时不想去维护一个开源项目,而且很多地方都像个半成品,截至这篇文章发布时,前端还不带样式,所以要自行手写CSS,当然可参考我网站的样式10。无论如何,这里还是简单写下如何部署使用吧。

目前Docker镜像发布到Docker Hub上了,直接拉过来就可以使用。可使用以下命令启动,注意修改命令中的数据存储路径,然后可以访问http://<hostname or ip>:8090/_打开PocketBase的后台管理页面。关于PocketBase的更多使用方法请参考官方文档

docker run -d \
  --name=pocketbase \
  -p 8090:8090 \
  -v /path/to/data:/pb_data \
  --restart unless-stopped \
  track3/pocketbase-comment:latest

如果不用Docker,就要自己编译了,这个请参考PocketBase README页面的Running and building部分,然后运行方法参考PocketBase的文档。

在PocketBase管理页面,打开设置,然后选择“Import collections”,然后导入这段JSON,PocketBase就配置好了。

PocketBase Import collections

然后前端只用在需要评论的地方加上以下html代码。当然生产环境建议配置HTTPS以及使用反向代理设置访问速率限制和CORS header等等,这里不作讨论。

<div id="comments" data-url="http://<hostname or ip>:8090/api/comment"></div>
<script type="module" crossorigin src="http://<hostname or ip>:8090/comment.js"></script>

如果看到下面这样的界面,不要怀疑是哪里出了问题,正常就是没有样式的,不信可以发个评论测试一下。

demo