当前位置 : 首页 » 文章分类 :  开发  »  Hexo博客(25)自建博客评论系统

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()"/>&nbsp
    <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

下一篇 Spring-Boot-Actuator

阅读
评论
3.8k
阅读预计16分钟
创建日期 2019-05-11
修改日期 2020-03-20
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论