前言

昨天写了篇给Typecho加上pjax的文章,现在再来介绍下ajax评论的部分(虽然也有大佬写过了)
如果你沒有js基础或者不太熟悉jquery请忽略本文
下面的代码适用于我自己写的这个主题,主要是提供一些思路和方法,具体要移植到你自己的模板上还需要具体测试和修改,也算是抛砖引玉吧 :)
一些错误希望各位大佬指正

正文

基本思路

监听评论表单的提交事件,使用ajax向typecho的后端发送评论的数据并取回服务端响应的内容(成功或失败),然后直接呈现在当前页面。
之前我在处理父级评论的时候,涉及到这样一个问题:

如果你在设置中:设置->评论->将较旧的评论显示在前面,那么ajax评论成功获取到数据之后你可能会面临如何处理父级评论翻页或不翻页的情况。
所以为了方便,在添加ajax评论的时候要将较新的评论显示在前面

这是我基本思路下基于jQuery的代码粗略结构,稍后会一部分一部分解析:

var replyTo = ''   //回复评论时候的ID
    submitButton = $(".submit").eq(0),  //提交评论按钮
    commentForm = $("#comment-form"),   //评论表单
    newCommentId = "",   //新评论的ID
    notyf = new Notyf({
                delay: 3000
            });     //评论成功或者失败的提示插件

var bindButton = function() {
    //绑定“评论回复”和“取消回复”的事件
    ...
}
bindButton();
function commentCounts() {
    //显示在评论区的评论数加一
    ...
}
function beforeSendComment() {
    //发送前的一些处理
    //比如加入一些过渡动画什么的
    ...
}
function afterSendComment() {
    //发送后的处理
    //清空replyTo变量,以及结束过渡动画、重新绑定回复按钮等等
    ...
}

$(commentForm).submit(function() {
    //监听评论表单submit事件
    var commentData = $(this).serializeArray(); //获取表单POST的数据
    beforeSendComment();
    $.ajax({
        type: $(this).attr('method'),
        url: $(this).attr('action'),
        data: commentData,
        error: function(e) {
            //失败的处理
            ...
        }
        success: function(data) {
            //判断评论成功或者失败
            //失败显示失败提示内容,然后直接返回false
            ...
            //发送成功进入判断
            //获取新评论的id
            var newComment; //新的评论的元素html内容
            newCommentId = $(".comment-list", data).html().match(/id=\"?comment-\d+/g).join().match(/\d+/g).sort(function (a, b) { return a - b }).pop();
            if( '' === replyTo ) {
                //处理父级评论
                if( !$('.comment-list').length ) {
                    //检查是否已有评论
                    //没有的话需要先嵌入评论列表的结构
                    ...
                }
                else if($('.prev').length) {
                    //当前评论页面不在第一页
                    ...
                }
                else {
                    //当前评论在第一页
                    ...
                }
                //一些处理
                ...
            }
            else {
                //处理子级评论
                if ($('#' + replyTo).find('.comment-children').length) {
                    //当前父评论已经有嵌套的结构
                    //直接插入新的评论
                    ...
                }
                else {
                    //当前父评论没有嵌套的结构
                    //先构建嵌套的结构再进插入子评论
                    ...
                }
            }
            commentCounts();//评论+1
            afterSendComment(true);
            notyf.confirm('评论提交成功!');//评论成功提示
        }
    })
    return false;
})

绑定评论回复按钮

var replyTo = '';
var bindButton = function() {
    //绑定“评论回复”和“取消回复”的事件
    $(".comment-reply a").click(
        function () {
            replyTo = $(this).parent().parent().parent().parent().attr("id");
        }
    );
    $(".cancel-comment-reply a").click(function () { replyTo = ''; });
}

绑定的回复和取消回复是这两个按钮:
typecho-ajaxcomment-1.png
replyTo这个变量应该是需要获取到这个id:
typecho-ajaxcomment-2.png
所以$(this).parent().parent().parent().parent().attr("id");应该根据评论结构的实际情况去修改,并且你也可以用不同的方式去拼接这个id字符串。

评论数目加一

function commentCounts() {
    //显示在评论区的评论数加一
    var counts = parseInt($("#response").text());
    $("#response").html($("#response").html().replace(/\d+/, counts + 1));
};

$("#response")这个选择器也是根据实际模板结构来的。
typecho-ajaxcomment-3.png

发送评论前后的处理

function beforeSendComment() {
    //发送前的一些处理
    //比如加入一些过渡动画什么的
    //评论框提交时候的动画
    submitButton.attr("disabled", true).css('cursor', 'not-allowed');
    commentForm.css({ 'opacity': '.5' });
    //THEME_CONFIG.THEME_URL是主题的路径
    $("textarea", commentForm).css({ 'background': 'url("' + THEME_CONFIG.THEME_URL + '/assets/img/loading.gif") center center no-repeat' });
    $("input,textarea", commentForm).attr('disabled', true);
}
function afterSendComment(ok) {
    //发送后的处理
    //清空replyTo变量,以及结束过渡动画、重新绑定回复按钮等等
    //ok作为一个评论或失败的标志

    //评论结束时的一些动画处理  
    submitButton.attr("disabled", false).css('cursor', 'pointer');
    commentForm.css({ 'opacity': '1' });
    $("textarea", commentForm).css({ 'background': 'initial' });
    $("input,textarea", commentForm).attr('disabled', false);

    if (ok) {
        $("#textarea").val('');
        replyTo = '';
    }
    bindButton();
}

主体部分解析

error 部分

beforeSendComment();
$(commentForm).submit(function() {
    //监听评论表单submit事件
    var commentData = $(this).serializeArray(); //获取表单POST的数据
    beforeSendComment();
    $.ajax({
        type: $(this).attr('method'),
        url: $(this).attr('action'),
        data: commentData,
        error: function(e) {
            //失败的处理
            //这里比较随意,比如可以直接刷新页面
            console.log('Ajax Comment Error');
            window.location.realod();
        }
        success: function(data) {
            ...
        }
    })
    return false;
});

success 部分

评论失败信息提示
success: function (data) {
    //评论失败提示框
    if (!$('#comments', data).length) {
        var msg = $('title').eq(0).text().trim().toLowerCase() === 'error' ? $('.container', data).eq(0).text() : '评论提交失败!';
        notyf.alert(msg);
        afterSendComment(false);
        return false;
    }
    ...
}

这个处理是在返回的数据中查找是否有下图评论错误的情况(未开启ajax评论),然后将其作为错误信息返回:
typecho-ajaxcomment-4.png

$('title').eq(0).text().trim().toLowerCase() === 'error' ? $('.container', data).eq(0).text()
父级评论处理
var newComment;
/** 获取新评论的id */
newCommentId = $(".comment-list", data).html().match(/id=\"?comment-\d+/g).join().match(/\d+/g).sort(function (a, b) { return a - b }).pop();
if(replyTo === '') {
        if(!$('.comment-list').length) {
            //检查是否已有评论
            newComment  = $("#li-comment-" + newCommentId, data);
            //没有的话需要先嵌入评论列表的结构
            //具体结构需要参照评论的模板而定,参照下图
            $('#response').after('<div class="comment-data"><ol class="comment-list"></ol></div>');
            //插入评论
            $('.comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
        }
        else if($('.prev').length) {
            //这里是当前评论不在第一页的情况
            //所以这里可以进行比如跳转到第一页的操作,当然也可以进行别的操作
            $('#page-nav ul li a').eq(1).click();
        }
        else {
            //当前页面直接在最前面插入评论
            newComment  = $("#li-comment-" + newCommentId, data);
            $('.comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
        }
        //页面滑动到评论列表头部
        $('html,body').animate({scrollTop:$('#response').offset().top - 100},1000);
    }

如下图,这个是在没有评论的时候应该嵌入的结构,具体应该根据你的模板而定

typecho-ajaxcomment-5.png

$('#response').after('<div class="comment-data"><ol class="comment-list"></ol></div>');
子级评论处理
else {
    //取数据
    newComment = $("#li-comment-" + newCommentId, data);
    //处理子级评论
    if ($('#' + replyTo).find('.comment-children').length) {
        //当前父评论已经有嵌套的结构
        //直接插入新的评论
        $('#' + replyTo + ' .comment-children .comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
        TypechoComment.cancelReply();
    }
    else {
        //当前父评论没有嵌套的结构
        //先构建嵌套的结构再进插入子评论
        //插入的结构视模板具体情况而定
        $('#' + replyTo).append('<div class="comment-children"><ol class="comment-list"></ol></div>');
        $('#' + replyTo + ' .comment-children .comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
        TypechoComment.cancelReply();
    }
}

关于评论的嵌套结构是根据你的模板情况而定的,例如我的:
typecho-ajaxcomment-6.png
故我的嵌套的代码:

('#' + replyTo).append('<div class="comment-children"><ol class="comment-list"></ol></div>');
$('#' + replyTo + ' .comment-children .comment-list').first().prepend((newComment).addClass('animated fadeInUp'));

关于TypechoComment.cancelReply();,要注意到这是Typecho评论的取消评论回复的js代码,这会将评论框恢复到原来最底下的时候(点击回复评论会出现在那条父评论的下方)
typecho-ajaxcomment-7.png

最后部分
commentCounts();
afterSendComment(true);
notyf.confirm('评论提交成功!');

至此ajax评论的代码基本完成了

完整代码

可能和上面的有些许出入,我自己用的稍微有点修改,可以作为参考自己改成自己要的效果
并且注释没有那么详细,需要详细注释请参考上面的每部分分析

ajaxComment: function () {
    var replyTo = ''   //回复评论时候的ID
    submitButton = $(".submit").eq(0),  //提交评论按钮
    commentForm = $("#comment-form"),   //评论表单
    newCommentId = "",   //新评论的ID
    notyf = new Notyf({
                delay: 3000
            });     //评论成功或者失败的提示插件
    var bindButton = function () {
        //console.log('调用bind');
        $(".comment-reply a").click(function () {
            replyTo = $(this).parent().parent().parent().parent().attr("id");
            //console.log(replyTo);
        });
        $(".cancel-comment-reply a").click(function () { replyTo = ''; });
        //console.log("bind button");
    };
    bindButton();

    /**
     * 评论数目加一
     */
    function commentCounts() {
        var counts = parseInt($("#response").text());
        $("#response").html($("#response").html().replace(/\d+/, counts + 1));
    };

    /**
     * 发送前的处理
     */
    function beforeSendComment() {
        submitButton.attr("disabled", true).css('cursor', 'not-allowed');
        commentForm.css({ 'opacity': '.5' });
        $("textarea", commentForm).css({ 'background': 'url("' + THEME_CONFIG.THEME_URL + '/assets/img/loading.gif") center center no-repeat' });
        $("input,textarea", commentForm).attr('disabled', true);
    }

    /**
     * 发送后的处理
     * @param {boolean} ok 
     */
    function afterSendComment(ok) {
        submitButton.attr("disabled", false).css('cursor', 'pointer');
        commentForm.css({ 'opacity': '1' });
        $("textarea", commentForm).css({ 'background': 'initial' });
        $("input,textarea", commentForm).attr('disabled', false);
        if (ok) {
            $("#textarea").val('');
            replyTo = '';
        }
        bindButton();
    }

    $("#comment-form").submit(function () {
        commentData = $(this).serializeArray();
        
        /** 准备发送数据 */
        beforeSendComment();
        $.ajax({
            type: $(this).attr('method'),
            url: $(this).attr('action'),
            data: commentData,
            error: function (e) {
                console.log('Ajax Comment Error');
                window.location.reload();
            },
            success: function (data) {
                if (!$('#comments', data).length) {
                    var msg = $('title').eq(0).text().trim().toLowerCase() === 'error' ? $('.container', data).eq(0).text() : '评论提交失败!';
                    notyf.alert(msg);
                    afterSendComment(false);
                    return false;
                }

                $("input,textarea", commentForm).attr('disabled', false);
                $("#textarea").val('');

                var newComment;
                /** 获取新评论的id */
                newCommentId = $(".comment-list", data).html().match(/id=\"?comment-\d+/g).join().match(/\d+/g).sort(function (a, b) { return a - b }).pop();
                /** 处理父级评论 */
                if('' === replyTo) {
                    if(!$('.comment-list').length) {
                        newComment  = $("#li-comment-" + newCommentId, data);
                        $('#response').after('<div class="comment-data"><ol class="comment-list"></ol></div>');
                        $('.comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
                    }
                    else if($('.prev').length) {
                        $('#page-nav ul li a').eq(1).click();
                    }
                    else {
                        newComment  = $("#li-comment-" + newCommentId, data);
                        $('.comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
                    }
                    $('html,body').animate({scrollTop:$('#response').offset().top - 100},1000);
                }
                /** 处理子评论 */
                else {
                    //取数据
                    newComment = $("#li-comment-" + newCommentId, data);
                    if ($('#' + replyTo).find('.comment-children').length) {
                        //已存在.comment-children 直接插入新数据
                        $('#' + replyTo + ' .comment-children .comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
                        TypechoComment.cancelReply();
                    }
                    else {
                        //创建一个.comment-children
                        $('#' + replyTo).append('<div class="comment-children"><ol class="comment-list"></ol></div>');
                        $('#' + replyTo + ' .comment-children .comment-list').first().prepend((newComment).addClass('animated fadeInUp'));
                        TypechoComment.cancelReply();
                    }
                }
                commentCounts();
                afterSendComment(true);
                notyf.confirm('评论提交成功!');
            }
        });
        return false;
    });
}

写在后面

本文参考过友人C的ajax评论的教程,具体是参考了下实现的思路,细节比如动画处理等有所不同。
还是那句话,添加ajax评论需要参照具体模板的结构。