本项目代码已开源,具体见:
后端工程:express-blog-backend
数据库初始化脚本:关注公众号程序员白彬,回复关键字“博客数据库脚本”,即可获取。
前端走向全栈,从这个项目开始准没错!
前言
上篇文章讲完了文章详情页的整体实现思路,但是唯独没有讲到评论的实现,因为我认为评论这个功能的实现用几百到一千的文字根本讲不清楚,必须要单独抽离出来,而且文章评论和留言板又有很多相通之处,或者说本质上是一样的!
实现方案
评论系统可以直接集成第三方的,在还没纯自研博客之前,我也搭建过基于 hexo 的博客站点,当时在评论系统的选型上也尝试过几种,其中大概可以按照是否需要身份认证来分类。
不需要进行身份认证的典型有 Valine,非常适合无后端的纯静态博客。
只需要填写上昵称、邮箱、网址和你的评论内容,就可以进行评论了,隐私性保护非常好,用户很容易接受这种方式!
细心的读者可能发现了,咱们明明没留下隐私信息,为什么评论区会出现头像?
这是因为 Valine 集成了 Gravatar,只要用户填写的邮箱和 Gravatar 账号的邮箱信息对应上,在评论区就能展示出该用户在 Gravatar 上设置的头像。(当然前提是用户注册过 Gravatar)
需要进行身份认证的第三方评论系统有 Gitment、livere、畅言等,集成了这些评论系统后,用户在发表评论前都需要先进行第三方登录或授权,评论数据会存储到第三方平台中,像 Gitment 类的评论系统是基于 github issue 实现的评论。
这类要授权的评论系统往往会因为网络或隐私性等问题劝退用户。
评论系统也可以纯自研实现,这通常需要耗费一些精力和时间,但是这给予了你足够大的自由度,你可以 DIY 出自己想要的任何效果!本开源博客项目也是采用的自研方式实现评论系统,我们一起来看看!
评论的要素分析
我截取了一篇文章的评论区作为案例,可以看到,评论涉及到人和内容两大块,其中人的信息包括昵称、头像、链接(通常是社交主页),内容信息包括评论内容、评论的关联信息(评论的是哪一篇文章,该评论是否是针对某条评论的回复)。
针对人的信息,首先要考虑是否需要用户进行登录认证。
如果需要登录认证,我们就得开发用户模块的相关功能,用户登录后可以进行评论,也可以查看自己的历史评论数据。
如果不需要登录认证,我们可以参考 Valine 的实现,让用户留下一些昵称、邮箱、网址之类的非强制绑定的信息,就能进行评论。用户留下的不是真实的邮箱也没关系,因为我们不太在意这些信息,更多的是在意评论内容,如果这个用户 ta 真的想和你互动,想必也会留下真实的邮箱。
我是比较倾向于选择第二种实现方式的,用户比较容易接受!
用户信息
由于我们不需要在服务端保存和验证用户信息,直接将用户信息存储在前端即可,可以考虑存储在 localStorage 中,方便用户第二次评论时自动带上。
同时给用户提供修改个人信息的能力。
评论的数据模型
评论内容部分我们怎么去设计呢?先观察评论的成品效果,我们知道,评论是一个瀑布流,本质上是数组,一个评论会有下级的回复,回复也是一个数组,回复可以是直接回复评论,也可以是对某条回复的回复。
那么在表的设计上,我是将评论和回复看做两个实体,通过单独的表 comment 和 reply 去维护。
评论除了基本的内容、状态等字段外,还会有一个article_id
用于外键关联文章,以便我们在查询时,能查询某篇文章下的评论数据。
而回复,它是依附于评论存在的,所以除了基本信息外,还会通过一个外键comment_id
关联到评论。
正如前文所言,回复可以是直接对评论进行回复,也可以是针对某条回复的回复,这需要我们设计一个parent_id
来找到其关联的回复。
撸码实现
实体的关系建立了之后,业务实现就会更加清晰,我们一起来看看!
以文章 id=234 为例说明
我们先从最简单的开始,先查询一篇文章所有的评论数据。
SELECT * FROM comments WHERE article_id = 234
就是这么简单粗暴!但是一篇文章的评论可能会很多,所以这里也要用到分页技术。
基于前面文章实战中的一些积累,分页对我们来说已经不是很难了,我们照猫画虎借鉴一下之前的实现。
我在语句中加了approved = 1
和deleted = 0
,approved 是审核状态,我们的评论还是要经过审核的,不能随便评论就展示出来。
在删除业务中,通常会分为物理删除和逻辑删除,deleted
就是一个逻辑删除标志位了,这是一个很常见的做法,防止数据记录被删除。
SELECT SQL_CALC_FOUND_ROWS * FROM comments
WHERE article_id = 234 AND approved = 1 AND deleted = 0
ORDER BY create_time DESC
LIMIT 0, 2;
SELECT FOUND_ROWS() AS total;
第一条语句返回分页数据:
总共执行了两条语句,第二条语句返回了这篇文章下的评论总数:
此时我们已经有了评论数据,但是评论下的回复,我们还没拿到。
针对评论下的回复,我们可以循环评论,再基于外键去查询关联这条评论的回复。
回复这里要注意的是:回复是由两部分组成的,一部分是评论下的一级回复,另一部分是评论下针对回复进行的回复。我们分开来查询这两部分数据,然后做一个联合。
首先查询评论下的一级回复,一级回复有个特点,parent_id
是空值 null,我们用条件外键查询来简单实现一下。
SELECT * FROM reply WHERE comment_id = 178 AND approved = 1 AND parent_id IS NULL
ORDER BY create_time ASC
另外一部分的实现可以参考评论,循环一级回复查询它的子级回复。
另一种方法是直接用连接查询写出来。reply 表连接 reply 表,条件是 parent_id 等于另一张表的 id。
SELECT a.*, b.nick_name AS reply_name FROM reply a
LEFT JOIN reply b
ON a.parent_id = b.id
WHERE a.comment_id = 178 AND a.parent_id IS NOT NULL AND a.approved = 1
ORDER BY create_time ASC
接着,我们把以上两种情况用 UNION 联合起来。
SELECT *, NULL as reply_name FROM reply WHERE comment_id = 178 AND approved = 1 AND parent_id IS NULL
UNION
SELECT a.*, b.nick_name AS reply_name FROM reply a
LEFT JOIN reply b
ON a.parent_id = b.id
WHERE a.comment_id = 178 AND a.parent_id IS NOT NULL AND a.approved = 1
ORDER BY create_time ASC
以上就是 id=178 的评论下的所有回复数据,我们只要按照数组的形式进行展示即可(实际上其中的部分回复是有上下级关系的)。
留言板的实现
了解了文章评论的实现后,实现留言板就比较简单了。
既然评论数据中 article_id 对应文章 id 代表着这篇文章下的评论数据,那我偷个懒,article_id 为 null 就代表是留言板的评论,这不就直接实现了一个留言板功能吗?简直是太机智了!
SELECT SQL_CALC_FOUND_ROWS * FROM comments
WHERE article_id IS NULL AND approved = 1 AND deleted = 0
ORDER BY create_time DESC
LIMIT 0, 10;
SELECT FOUND_ROWS() AS total;
思路扩展
假设评论这个功能不仅仅在文章和留言板中出现,也会出现在其他业务中,应该怎么优化我们的设计呢?
目前,我们在 comments 表中通过article_id
来区分文章评论或者是留言板评论,article_id
有值代表是文章评论,空值 null 代表是留言板评论。这使得评论这个功能只能用于这两个场景,无法扩展。
为了扩展到更多的场景,我们可以舍弃掉article_id
这种字段,新增biz_type
和biz_id
字段,具体用法:
- 当 biz_type = "article" 时,comments 记录为文章评论,biz_id 就是文章的 id。
- 当 biz_type = "board" 时,comments 记录为留言板评论,biz_id 可以不填。
- 扩展举例:当 biz_type = "community" 时,comments 记录为社区评论,biz_id 可以是社区帖子的 id。
小结
本文主要分享了我在设计评论功能时的一些思路和实现过程,不仅仅介绍了文章评论和留言板评论,还进行了一点思维拓展,希望对大家有帮助!