TYPECHO实现AJAX评论功能

林尽欢 编程技术 阅读量

写在前面

给主题JAYDENFORU添加评论系统的时候,就想到了无刷新评论~因此有了此文。

思路

  1. 监听评论表单,改用AJAX方式提交
  2. 创建新的评论表单提交地址(用JS实现)

First

当访问文章加载主题时,themeInit方法首先被加载,可在此方法中判断是否为添加评论的操作,即新的评论表单地址为文章的链接(permalink).具体判断方法如下

// 主题初始化
function themeInit($archive){
    // 判断是否是添加评论的操作
    // 为文章或页面、post操作,且包含参数`Ajax=comment`(自定义)
    if($archive->is('single') && $archive->request->isPost() && $archive->request->is('Ajax=comment')){
        // 为添加评论的操作时
        ajaxComment($archive);
    }
}

要实现AJAX评论,就无法使用TYPECHO自带输出评论函数了,这里我们将复制TYPECHO的$comments功能并改造为我们需要的FUNCTION

/**
 * ajaxComment
 * 实现Ajax评论的方法(实现feedback中的comment功能)
 * @param Widget_Archive $archive
 * @return void
 */
function ajaxComment($archive)
{
  $options = Helper::options();
  $user = Typecho_Widget::widget('Widget_User');
  $db = Typecho_Db::get();
  // Security 验证不通过时会直接跳转,所以需要自己进行判断
  // 需要开启反垃圾保护,此时将不验证来源
  if ($archive->request->get('_') != Helper::security()->getToken($archive->request->getReferer())) {
    $archive->response->throwJson(array('status' => 0, 'msg' => _t('非法请求')));
  }
  /** 评论关闭 */
  if (!$archive->allow('comment')) {
    $archive->response->throwJson(array('status' => 0, 'msg' => _t('评论已关闭')));
  }
  /** 检查ip评论间隔 */
  if (!$user->pass('editor', true) && $archive->authorId != $user->uid &&
    $options->commentsPostIntervalEnable) {
    $latestComment = $db->fetchRow($db->select('created')->from('table.comments')
      ->where('cid = ?', $archive->cid)
      ->where('ip = ?', $archive->request->getIp())
      ->order('created', Typecho_Db::SORT_DESC)
      ->limit(1));

    if ($latestComment && ($options->gmtTime - $latestComment['created'] > 0 &&
        $options->gmtTime - $latestComment['created'] < $options->commentsPostInterval)) {
      $archive->response->throwJson(array('status' => 0, 'msg' => _t('对不起, 您的发言过于频繁, 请稍侯再次发布')));
    }
  }

  $comment = array(
    'cid' => $archive->cid,
    'created' => $options->gmtTime,
    'agent' => $archive->request->getAgent(),
    'ip' => $archive->request->getIp(),
    'ownerId' => $archive->author->uid,
    'type' => 'comment',
    'status' => !$archive->allow('edit') && $options->commentsRequireModeration ? 'waiting' : 'approved'
  );

  /** 判断父节点 */
  if ($parentId = $archive->request->filter('int')->get('parent')) {
    if ($options->commentsThreaded && ($parent = $db->fetchRow($db->select('coid', 'cid')->from('table.comments')
        ->where('coid = ?', $parentId))) && $archive->cid == $parent['cid']) {
      $comment['parent'] = $parentId;
    } else {
      $archive->response->throwJson(array('status' => 0, 'msg' => _t('父级评论不存在')));
    }
  }
  $feedback = Typecho_Widget::widget('Widget_Feedback');
  //检验格式
  $validator = new Typecho_Validate();
  $validator->addRule('author', 'required', _t('必须填写用户名'));
  $validator->addRule('author', 'xssCheck', _t('请不要在用户名中使用特殊字符'));
  $validator->addRule('author', array($feedback, 'requireUserLogin'), _t('您所使用的用户名已经被注册,请登录后再次提交'));
  $validator->addRule('author', 'maxLength', _t('用户名最多包含200个字符'), 200);

  if ($options->commentsRequireMail && !$user->hasLogin()) {
    $validator->addRule('mail', 'required', _t('必须填写电子邮箱地址'));
  }

  $validator->addRule('mail', 'email', _t('邮箱地址不合法'));
  $validator->addRule('mail', 'maxLength', _t('电子邮箱最多包含200个字符'), 200);

  if ($options->commentsRequireUrl && !$user->hasLogin()) {
    $validator->addRule('url', 'required', _t('必须填写个人主页'));
  }
  $validator->addRule('url', 'url', _t('个人主页地址格式错误'));
  $validator->addRule('url', 'maxLength', _t('个人主页地址最多包含200个字符'), 200);

  $validator->addRule('text', 'required', _t('必须填写评论内容'));

  $comment['text'] = $archive->request->text;

  /** 对一般匿名访问者,将用户数据保存一个月 */
  if (!$user->hasLogin()) {
    /** Anti-XSS */
    $comment['author'] = $archive->request->filter('trim')->author;
    $comment['mail'] = $archive->request->filter('trim')->mail;
    $comment['url'] = $archive->request->filter('trim')->url;

    /** 修正用户提交的url */
    if (!empty($comment['url'])) {
      $urlParams = parse_url($comment['url']);
      if (!isset($urlParams['scheme'])) {
        $comment['url'] = 'http://' . $comment['url'];
      }
    }

    $expire = $options->gmtTime + $options->timezone + 30 * 24 * 3600;
    Typecho_Cookie::set('__typecho_remember_author', $comment['author'], $expire);
    Typecho_Cookie::set('__typecho_remember_mail', $comment['mail'], $expire);
    Typecho_Cookie::set('__typecho_remember_url', $comment['url'], $expire);
  } else {
    $comment['author'] = $user->screenName;
    $comment['mail'] = $user->mail;
    $comment['url'] = $user->url;

    /** 记录登录用户的id */
    $comment['authorId'] = $user->uid;
  }

  /** 评论者之前须有评论通过了审核 */
  if (!$options->commentsRequireModeration && $options->commentsWhitelist) {
    if ($feedback->size($feedback->select()->where('author = ? AND mail = ? AND status = ?', $comment['author'], $comment['mail'], 'approved'))) {
      $comment['status'] = 'approved';
    } else {
      $comment['status'] = 'waiting';
    }
  }

  if ($error = $validator->run($comment)) {
    $archive->response->throwJson(array('status' => 0, 'msg' => implode(';', $error)));
  }

  /** 添加评论 */
  if (preg_match("/[\x{4e00}-\x{9fa5}]/u", $comment['text']) == 0) {
    $archive->response->throwJson(array('status' => 0, 'msg' => _t('评论内容请不少于一个中文汉字')));
  }
  $commentId = $feedback->insert($comment);
  if (!$commentId) {
    $archive->response->throwJson(array('status' => 0, 'msg' => _t('评论失败')));
  }
  Typecho_Cookie::delete('__typecho_remember_text');
  $db->fetchRow($feedback->select()->where('coid = ?', $commentId)
    ->limit(1), array($feedback, 'push'));
  $feedback->pluginHandle()->finishComment($feedback);
  // 返回评论数据
  $data = array(
    'cid' => $feedback->cid,
    'coid' => $feedback->coid,
    'parent' => $feedback->parent,
    'mail' => $feedback->mail,
    'url' => $feedback->url,
    'ip' => $feedback->ip,
    'browser' => getBrowser($feedback->agent),
    'os' => getOs($feedback->agent),
    'author' => $feedback->author,
    'authorId' => $feedback->authorId,
    'permalink' => $feedback->permalink,
    'created' => $feedback->created,
    'datetime' => $feedback->date->format('Y-m-d H:i:s'),
    'status' => $feedback->status,
  );
  // 评论内容
  ob_start();
  $feedback->content();
  $data['content'] = ob_get_clean();

  $data['avatar'] = Typecho_Common::gravatarUrl($data['mail'], 48, Helper::options()->commentsAvatarRating, NULL, $archive->request->isSecure());
  $archive->response->throwJson(array('status' => 1, 'comment' => $data));
}

当已经在functions.php文件中添加完上述FUNCTION后,我们就可以进行下一步构造了~
在footer.php添加如下

<script>
var postUrl = location.protocol + '//' + location.host + location.pathname;// 获取文章路径
// 这一步是子评论的监控回复按钮事件 回复的BUTTON添加onclick属性 onclick='javascript:replyMsg(传入子评论ID,子评论的AUTHOR)'
const replyMsg = (id, author) => {
  const tmp = id.replace("comment-", "");
  $("#veditor").focus();
  $('html,body').animate({scrollTop:$('.blog-post-comments').offset().top-70}, 500);
  $("#veditor").attr("placeholder", "@" + author);
  $("#veditor").attr("parent", id);
  $("#comment-form").attr("action", postUrl + "/comment?parent=" + tmp);
};
// 依赖jquery,请自行加载
$(() => {
  // 监听评论表单提交
  $('#comment-form').submit(() => {
    let params = $('#comment-form').serialize();
    // 添加functions.php中定义的判断参数
    params += '&Ajax=comment';
    // 解析新评论并附加到评论列表
    const appendComment = (comment) => {
      // 评论列表
      let el = $('.comment-list');
      if (comment.parent != 0) {
        // 子评论则重新定位评论列表
        el = $('#comment-' + comment.parent);
        // 父评论不存在子评论时
        if (el.find('.comment-list').length < 1) {
          $('<ol class="comment-list" id="parent-' + comment.parent +'"></ol>').appendTo(el);
        }
        el = el.find('#parent-' + comment.parent);
      }
      if (el.length == 0) {
        $('<div class="vlist"><div class="vcard"></div><ol class="comment-list"></ol></div>').appendTo($('#comments'));
        el = $('#comments > .comment-list');
      }
      // 评论html模板,根据具体主题定制
      let html = '<div class="vlist" id="comment-{coid}"><div class="vcard" id="c-{coid}"><img class="vimg" src="{avatar}"><div class="vh"><div class="vhead"><span class="vnick"><a href="{url}" rel="external nofollow">{author}</a></span><span class="vsys">{browser}</span>&nbsp;<span class="vsys">{os}</span></div><div class="vmeta"><span class="vtime">{datetime}</span><span class="vat"><a href="javascript:void(0);" onclick="replyMsg(\'{coid}\',\'{author}\')">回复</a></span></div><div class="vcontent">{content}</div></div></div></div>\n';
      if ($("#veditor").attr("parent") != null) {
        html = '<div class="vlist" id="comment-{coid}"><div class="vcard" id="c-{coid}"><img class="vimg" src="{avatar}"><div class="vh"><div class="vhead"><span class="vnick"><a href="{url}" rel="external nofollow">{author}</a></span><span class="vsys">{browser}</span>&nbsp;<span class="vsys">{os}</span></div><div class="vmeta"><span class="vtime">{datetime}</span><span class="vat"><a href="javascript:void(0);" onclick="replyMsg(\'{coid}\',\'{author}\')">回复</a></span></div><div class="vcontent"><p><a href="#comment-{parent}">@{author}</a> {content}</p></div></div></div></div>\n';
      }
      $.each(comment, (k, v) => {
        regExp = new RegExp('{' + k + '}', 'g');
        html = html.replace(regExp, v);
      });
      $(html).appendTo(el);
    };
    // ajax提交评论
    $.ajax({
      url: $("form").attr("action"),
      type: 'POST',
      data: params,
      dataType: 'json',
      beforeSend: () => {
        $('#comment-form').find('.vsubmit').addClass('loading').html('<i class="icon icon-loading icon-pulse"></i> 提交中...').attr('disabled', 'disabled');
      },
      complete: () => {
        $('#comment-form').find('.vsubmit').removeClass('loading').html('提交评论').removeAttr('disabled');
      },
      success: (result) => {
        if (result.status == 1) {
          // 新评论附加到评论列表
          appendComment(result.comment);
          $('#comment-form').find('textarea').val('');
        } else {
          // 提醒错误消息
          alert(undefined === result.msg ? '评论出错' : result.msg);
        }
      },
      error: (xhr, ajaxOptions, thrownError) => {
        alert('评论失败,请重试');
      }
    });
    return false;
  });
});
</script>

此外评论表单的回复BUTTON需要把class改为vsubmit或者修改代码自定义class属性

遇到的问题

  1. 邮件通知插件不发出通知
  2. 评论过滤插件失效

How to solve

邮件通知插件不发出通知

原因是因为重写了评论的函数,而函数中没写评论完成后触发插件接口,所以邮件通知插件不会发邮件给AUTHOR

在$db->fetchRow($feedback->select()->where('coid = ?', $commentId)->limit(1), array($feedback, 'push'));的后面添加
$feedback->pluginHandle()->finishComment($feedback);

评论过滤插件失效

原因是没有写入评论过滤的接口,解决方法例如:
在$commentId = $feedback->insert($comment);前面添加

if (preg_match("/[\x{4e00}-\x{9fa5}]/u", $comment['text']) == 0) {
$archive->response->throwJson(array('status'=>0,'msg'=>_t('评论内容不得少于一个字符')));
}

OR你可以照葫芦画瓢,写自己的规则~

喵~
林尽欢
我最不喜欢做选择,但我选择了,就一定不后悔。
TYPECHO文章页/独立页实现lazyload懒加载完美
TYPECHO文章页/独立页实现lazyload懒加载完美

TYPECHO实现图片懒加载

1 分钟阅读
GHOST图片优化
GHOST图片优化

折腾一个晚上,终于配置好GHOST。服务器辣鸡,图片太多会拖慢响应速度,于是就打算配置七牛云镜像存储,GITHUB找到一个项目[QN-STORE](https://github.com/minwe/qn-store)插件,直接拿来用吧~

2 分钟阅读