Hexo博客(25)自建博客评论系统
买了VPS后就想着搞点什么,一直对第三方评论系统的各种限制和经常关停感到不满,从一开始的 多说 到 网易云跟帖 到 来必力,换来换去的,而且有各种限制,评论内容还不好迁移,就用业余时间自己搭建一个评论系统。也是我长远的整套后台服务体系中的一部分。
技术架构上,
前台是jQeury,以插件的形式嵌入到现有的静态博客页面中。
后台是Spring Boot实现的RESTful接口,评论内容存储在MySQL中,是一套非常成熟的后端框架。
总体来说,感觉还是后端复杂一点,前端刚开始无从下手,写过一些后感觉还好。自建评论系统上线后,也就正式把 来必力 去掉了。
后端
刚开始设计了用户系统和评论系统,评论时填入email就自动注册为用户,后来感觉有点儿过量设计,还没有做注册和登录,设计用户系统没什么意义,就改为先只做一个评论系统。
评论表
评论表的设计前后改了好几版,一开始想着用文章的URI做评论所属页面标识,但缺点是只有文章页面能评论,主页、类别、标签页无法评论,而且留言页面还得特殊处理,后来改为用整个url中host后面的pathname做标识,所有页面都可以评论。
-- 选择数据库
USE blog;
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'id,自增主键',
`pid` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '父评论id',
`pathname` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '评论对应的页面pathname',
`host` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '评论所在站点的域名',
`nickname` VARCHAR(256) CHARACTER SET utf8mb4 COMMENT '评论者昵称',
`email` VARCHAR(64) COMMENT '评论者邮箱',
`ip` VARCHAR(128) COMMENT '评论者ip',
`content` TEXT CHARACTER SET utf8mb4 COMMENT '评论内容',
`enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否有效数据',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
-- mysql最大索引 768 个字节, utf8 占 3 个字节,768/3=256
KEY `pathname` (`pathname`(255))
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
接口
目前只做了两个接口,一个新建评论,一个根据条件查询评论列表。
API 文档如下,使用 Api2Doc 自动生成了文档:
http://api.madaimeng.com/api2doc/home.html
遇到的问题
1、Linux时区没改为东八区,导致创建的评论时间都多8个小时。修改Linux时区后解决。
2、每次修改表结构后,MyBatis生成的mapper代码每次都追加到现有文件中而不是直接覆盖。增加了一个MyBatis覆盖插件后解决。
3、开发Spring接口相关的一些问题,平时工作一直在照着套路写接口,但自己写起来还是遇到了不少问题。
前端
前端写起来真的好痛苦,后端接口写好了,但前端怎么和后端交互都不知道,只知道个ajax,具体怎么写不太懂。
边写边学,逐渐把架子搭起来了。
1、跨域请求。
我前端页面和后端接口不同源,后端接口都在api.
子域名上,首先要解决的就是跨域请求问题。好在也很好处理,前端不做任何改动,后端在Nginx配置CORS,把允许访问的域名配置上就行。
可以参考这篇笔记 同源策略和CORS跨域
2、jQuery相关的一些操作不熟悉,边写边查,目前做些简单的DOM读取和修改操作,简单的事件响应都没问题了。
发表评论
1、静态html在页面生成评论框。评论按钮的响应事件指向 postComment()
js函数。
<!-- 评论框 -->
<div id="div_form_comment" class="div-form-comment">
<form id="form_comment">
<textarea id="textarea_comment_content" style="width:100%; overflow:auto"></textarea>
<input type="text" id="input_nickname" placeholder="昵称(非必须)"/>
<input type="text" id="input_email" placeholder="Email(非必须)"/>
<input type="button" value="评论" onclick="postComment()"/> 
<input type="reset" value="重置" />
</form>
</div>
2、输入评论点击提交后调用postComment()
函数,发送Ajax Post请求,调用后端创建评论接口。
<script type="text/javascript">
// 发表评论
function postComment() {
$.ajax({
type: "POST",
dataType: "json", //服务器返回的数据类型
contentType: "application/x-www-form-urlencoded", //post请求的信息格式
url: BACKEND_SERVER + "comments", // 创建评论接口api
data: {
'pathname': window.location.pathname,
'nickname':$('#input_nickname').val(),
'email':$('#input_email').val(),
'content':$('#textarea_comment_content').val()
},
success: function (result) {
console.log(result);//在浏览器中打印服务端返回的数据(调试用)
if (result.resultCode == 200) {
console.log("SUCCESS");
};
// 评论成功后触发一次查询
getCommentsByPathname();
},
error : function() {
alert("发表评论异常!");
}
});
}
</script>
3、如何实现评论后立即出现在当前页面上的?
评论成功后立即查询一次当前页面评论。getCommentsByPathname()
函数就是查询当前页面评论。
其实想要效率高点儿的话,可以后端修改发表评论接口让其返回新发表的评论,直接从返回中拿到新增加的评论 append 到当前页面的评论列表,但我为了省劲(因为已经写好了查询评论列表函数)直接又调用了一次查询接口,多一次交互请求。
查询当前页面的评论
发送Ajax Get请求,查询当前页面的评论,根据后端接口返回的json生成评论列表,同时获取评论个数写到侧边栏。生成的评论列表支持父子评论树形层级结构。
1、首先后端接口返回的json就得是树形的,从数据库查出数据后,根据每个评论的pid写了个递归组装的数据,返回样例如下:
{
"amount":3,
"comments":[
{
"id":2,
"pid":0,
"pathname":"/article/hexo-23-change-image-repo/",
"nickname":"寐宕先生",
"content":"和你情况一样,不靠谱的七牛云,不过我是直接过期了连下载都不能下载了。但是本地有备份,我的解决方案是用oneindex,你的这个方式也不错,先马!",
"enabled":true,
"child_comments":[
{
"id":3,
"pid":2,
"pathname":"/article/hexo-23-change-image-repo/",
"nickname":"龙猫",
"content":"oneindex这个方案也不错哦",
"enabled":true,
"child_comments":[
],
"create_time":"2019-03-10 06:39:46",
"update_time":"2019-03-10 06:39:46"
}
],
"create_time":"2019-03-10 06:39:43",
"update_time":"2019-03-10 06:39:43"
},
{
"id":1,
"pid":0,
"pathname":"/article/hexo-23-change-image-repo/",
"nickname":"AlexanderKing",
"content":"我也是七牛云图片不能用的。都说七牛图片服务不错,现在装的picgo啥的客户端也没啥用了。正如文章所说以后写博客 还是少用图片。oneindex我也来瞅瞅。",
"enabled":true,
"child_comments":[
],
"create_time":"2019-03-10 06:39:40",
"update_time":"2019-03-10 06:39:40"
}
]
}
2、根据pathname查询评论列表并展示在div_comments
这个函数中先调用后端接口根据当前页面的pathname查询评论列表json数据,然后:
(1)递归生成树形评论列表html内容
(2)写入评论展示div
(3)评论个数填入侧边栏
(4)给所有“回复”按钮添加点击事件响应,实现点击后能够生成回复框等。
<script type="text/javascript">
// 根据pathname查询评论列表并展示在div_comments
function getCommentsByPathname() {
var api;
if (window.location.pathname.toLowerCase() === "/message/") {
// 留言页面查询全站评论列表
var api = BACKEND_SERVER + "comments";
} else {
// 其他页面只查询当前页面的评论列表
var api = BACKEND_SERVER + "comments?pathname=" + encodeURIComponent(window.location.pathname);
}
$.ajax({
// 请求方式
type: "GET",
// 根据pathname查询评论GET接口api
url: api,
// 返回数据格式
dataType: "json",
// 请求成功后要执行的函数,response即为返回的json数据
success: function(response){
// 生成评论列表html内容
var commentsHtml = generateCommentsHtmlByJson(response.comments);
// 写入评论展示div
$("#div_comments").html(commentsHtml);
// 评论个数填入侧边栏
$("#comments-amount").html(response.amount);
// 给所有“回复”按钮添加点击事件响应,实现点击后能够生成回复框等
addEventToReplyButton();
}
});
}
</script>
这个函数中还调用了另外两个,一个是 generateCommentsHtmlByJson()
负责具体的html拼接,会递归解析树形评论
<script type="text/javascript">
// 遍历json评论列表comments,生成评论html内容
function generateCommentsHtmlByJson(comments) {
var str = "";
// 遍历comments中的评论列表
$.each(comments, function(i, comment){
// 单条评论div开始
str += `<div class="comment" comment-id="${comment.id}" comment-pathname="${comment.pathname}">`;
// 评论header
str += `<div class="comment-header">`;
str += `<span> <i class="fa fa-user-circle-o"></i> ${comment.nickname}`;
if (comment.ip !== undefined && comment.ip !== null && comment.ip !== '') {
str += ` [${comment.ip}] `
}
if (comment.email !== undefined && comment.email !== null && comment.email !== '') {
str += `<i class="fa fa-envelope-o"></i> ${comment.email.toLowerCase()}`
}
str += "</span>";
// 评论所属页面链接
str += `<span style="float:right;"><a href="${window.location.protocol}//${window.location.host}${comment.pathname}"><span class="fa fa-link"></span> ${comment.pathname}</a></span>`;
str += "</div>";
// 评论内容div
str += `<div class="comment-content">`;
str += `<span>${comment.content}</span>`;
str += "</div>";
// 评论footer
str += `<div class="comment-footer">`;
str += `<span><i class="fa fa-calendar-plus-o"></i> ${comment.create_time}</span>`;
// 回复按钮
str += `<span style="float:right;"><button class="reply-button" comment-id="${comment.id}" pathname="${comment.pathname}">回复</button></span>`;
str += "</div>";
// 递归查询当前评论的子评论div
if (comment.child_comments !== undefined && comment.child_comments.length > 0) {
str += generateCommentsHtmlByJson(comment.child_comments);
}
// 单条评论div结束
str += "</div>";
});
return str;
}
</script>
回复评论
在评论列表的每个评论块上动态创建“回复”按钮,实现对某一条评论的回复,这里确实卡了我一段时间
1、“回复”按钮是根据返回json动态生成的,有多少条评论就有多少个“回复”按钮,通过在组装评论列表html时给每条评论加个<button class="reply-button">
实现,给所有回复按钮都加了类 reply-button
,方便通过class选择。
2、还得实现点击“回复”按钮能弹出评论框,并且这个评论框是用来回复这条评论的,也就是回复内容会成为这条评论的child。而且展开的评论框还得能收回,为此需要展开评论框后按钮的text变为“取消”。这些都是在按钮的响应事件中实现的,在 addEventToReplyButton()
函数中给所有“回复”按钮添加响应事件,每次点击时要判断并修改按钮文本,还要判断当前页面上是否有其他回复框,有就删除,确保页面中同时只有一个回复框。
<script type="text/javascript">
// 给所有“回复”按钮添加点击事件响应,实现点击后能够生成回复框等
function addEventToReplyButton() {
$('.reply-button').each(function() {
// console.log($(this).attr("comment-id"));
$(this).on('click', function (event) {
// event 就是点击的按钮
console.log("点击了 " + $(event.target).attr("pathname") + " 评论ID " + $(event.target).attr("comment-id") + $(event.target).text());
// 按钮的文本内容为“回复”
if ($(event.target).text() == "回复") {
// 将现有回复框div(如果有的话)对应的按钮文本改为“回复”
$('#div_reply_comment').parent().find('.reply-button').text("回复");
// 删除现有回复框div(如果有的话)
$('#div_reply_comment').remove();
// 生成新回复评论输入框
var replyDiv = generateReplyCommentHtml($(event.target));
// 将回复框append到button的父父节点上
$(event.target).parent().parent().append(replyDiv);
// 将“回复”按钮的文本内容改为“取消”
$(event.target).text("取消");
} else if($(event.target).text() == "取消") {
// 删除回复框div
$('#div_reply_comment').remove();
// 将按钮的文本内容改为“回复”
$(event.target).text("回复");
}
});
});
}
</script>
具体的回复框html是通过 generateReplyCommentHtml()
函数生成的:
<script type="text/javascript">
// 生成回复评论输入框,replyButton 是“回复”按钮
function generateReplyCommentHtml(replyButton) {
// 获取回复按钮的属性
var parentCommentId = replyButton.attr("comment-id");
var parentCommentPathname = replyButton.attr("pathname");
// 生成新的回复div
var form = $(`<form id="form_comment" pid="${parentCommentId}"></form>`);
form.attr("pathname", parentCommentPathname)
form.append(`<textarea id="textarea_comment_content" style="width:100%; overflow:auto"></textarea>`);
form.append(`<input type="text" id="input_nickname" placeholder="昵称(非必须)"/>`);
form.append(`<input type="text" id="input_email" placeholder="Email(非必须)"/>`);
// 生成评论按钮并绑定点击事件
var button = $(`<input type="button" value="评论" />`);
button.on('click', function(event) {
replyComment(event);
});
form.append(button);
form.append(`<input type="reset" value="重置" />`);
var div = $(`<div id="div_reply_comment" class="div-form-comment"></div>`)
div.append(form);
return div;
}
</script>
回复框上会带上父评论的id和pathname,方便回复时传给后端,具体的回复逻辑在 replyComment()
函数中,在点击回复框上的“评论”按钮时触发。
相比直接发表评论,回复别人的评论只是多给后端传个pid参数,告诉后端当前这条评论是谁的child,其实调用的是同一个创建评论接口。
<script type="text/javascript">
// 回复评论,event是“评论”按钮
function replyComment(event) {
console.log("点击了评论PID " +$(event.target).parent().attr("pid") +" 的提交按钮");
// "评论"按钮的父节点,即评论form
var commentForm = $(event.target).parent();
$.ajax({
type: "POST",
dataType: "json", //服务器返回的数据类型
contentType: "application/x-www-form-urlencoded", //post请求的信息格式
url: BACKEND_SERVER + "comments", // 创建评论接口api
data: {
'pathname': commentForm.attr("pathname"),
'pid': commentForm.attr("pid"),
'nickname':commentForm.children("#input_nickname").val(),
'email':commentForm.children('#input_email').val(),
'content':commentForm.children('#textarea_comment_content').val()
},
success: function (result) {
console.log(result);//在浏览器中打印服务端返回的数据(调试用)
if (result.resultCode == 200) {
console.log("SUCCESS");
};
// 评论成功后触发一次查询
getCommentsByPathname();
},
error : function(jqXHR, textStatus, errorThrown) {
alert("评论异常");
console.log(jqXHR.responseText);
console.log(textStatus);
console.log(errorThrown);
}
});
}
</script>
使用marked渲染评论内容
markedjs / marked
https://github.com/markedjs/marked
head.ejs 中增加 marked.min.js
<!-- 2020.3.21 引入 marked markdown 解析 -->
<script src="<%- config.root %>js/marked.min.js"></script>
对于后台返回的评论内容,先用 marked()
函数渲染一下,再展示即可
// 使用 marked.min.js 进行 markdown 渲染
var markdownContent = marked(comment.content);
str += `<span>${markdownContent}</span>`;
上一篇 Apache-RocketMQ
页面信息
location:
protocol
: host
: hostname
: origin
: pathname
: href
: document:
referrer
: navigator:
platform
: userAgent
: