解决 Typecho 使用 PJAX/InstantClick 导致的评论问题

0、问题描述

引入 PJAX / InstantClick 等异步加载的脚本后,切换页面时出现以下异常状况:

  1. 无法点击“回复”按钮;
  2. 评论失败,返回(刷新页面)后重新评论正常。

问题 2 的问题在于 Typecho 的“检查评论来源页 URL 是否与文章链接一致”和“反垃圾保护”和主题、PJAX、InstantClick 的设置不一定兼容,问题 1 的问题是由于 Typecho 在<head>中输出TypechoComment的内容,而 PJAX / InstantClick 一般只重载<body>

1、解决方法

1.1、问题1

首先在 Typecho 后台的 设置-评论-评论提交关闭“检查评论来源页 URL 是否与文章链接一致”和“反垃圾保护”。

我知道关闭之后会被机器人刷垃圾评论,因此我推荐使用评论验证码或评论反垃圾插件,例如我之前用了很久的 reCAPTCHAv3 插件。
最近我发现,由于 ReCaptcha 镜像站出了点问题,不是“无法连接到 ReCaptcha” 就是“你被判定为了机器人”,几乎无法使用,于是换成了 Cloudflare Turnstile,申请、配置参考本站文章:

(还没写)

接下来兼容 PJAX / InstantClick:

如果使用 PJAX,编辑/var/Widget/Security.php第 50 行,改为:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getTokenUrl($path)
{
    $parts = parse_url($path);
    $params = array();

    if (!empty($parts['query'])) {
        parse_str($parts['query'], $params);
    }

    $params['_'] = $this->getToken(preg_replace('/\?_pjax=%23body/', '', $this->request->getRequestUrl()));
    $parts['query'] = http_build_query($params);

    return Typecho_Common::buildUrl($parts);
}

然后将 PJAX / InstantClick 的刷新函数(页面更改后执行的函数)中加入:

JavaScript
1
2
var now_url_comment = location.href + ' ';
history.replaceState(null,'',now_url_comment.replace('/comment ',''));

即可解决问题。

1.2、问题2

解决 JavaScript 加载问题(回复按钮的问题),有两种思路:

  1. 更新页面时重新加载函数;
  2. 直接删除 Typecho 自带的评论函数,修改为适配自己模板的函数。

1.2.1、思路1

第一种方法更通用,只需在输出评论时添加:

PHP
1
<script>respondId = '<?php $this->respondId(); ?>';</script>

如果模板的此处没有$this(例如我的),可以用 JS 获得。例如评论表单是:

PHP
1
2
3
<div id="<?php $this->respondId(); ?>" class="response-box">
...
</div>

可以将上述输出代码改为:

PHP
1
<script>respondId = document.getElementsByClassName("response-box")[0].id;</script>

无论哪种方法,输出后页面中 responseId 将加载到 JS 的变量中,因此只需在 PJAX / InstantClick 的刷新函数中加入:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
window.TypechoComment = {
    dom : function (id) {
        return document.getElementById(id);
    },

    create : function (tag, attr) {
        var el = document.createElement(tag);

        for (var key in attr) {
            el.setAttribute(key, attr[key]);
        }

        return el;
    },

    reply : function (cid, coid) {
        var comment = this.dom(cid), parent = comment.parentNode,
            response = this.dom(respondId()), input = this.dom('comment-parent'),
            form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
            textarea = response.getElementsByTagName('textarea')[0];

        if (null == input) {
            input = this.create('input', {
                'type' : 'hidden',
                'name' : 'parent',
                'id'   : 'comment-parent'
            });

            form.appendChild(input);
        }

        input.setAttribute('value', coid);

        if (null == this.dom('comment-form-place-holder')) {
            var holder = this.create('div', {
                'id' : 'comment-form-place-holder'
            });

            response.parentNode.insertBefore(holder, response);
        }

        comment.appendChild(response);
        this.dom('cancel-comment-reply-link').style.display = '';

        if (null != textarea && 'text' == textarea.name) {
            textarea.focus();
        }

        return false;
    },

    cancelReply : function () {
        var response = this.dom(respondId()),
        holder = this.dom('comment-form-place-holder'), input = this.dom('comment-parent');

        if (null != input) {
            input.parentNode.removeChild(input);
        }

        if (null == holder) {
            return true;
        }

        this.dom('cancel-comment-reply-link').style.display = 'none';
        holder.parentNode.insertBefore(response, holder);
        return false;
    }
};

如果仍不生效,可以根据自己的模板内容稍作修改。

1.2.2、思路2

我不太喜欢 Typecho 回复评论时,把评论框 append 到了要回复的主题下面,因此我直接将自带的评论函数删去,即把<head>部分的:

PHP
1
$this->header('');

改为:

PHP
1
$this->header('commentReply=');

当然,我推荐把头部一大堆乱七八糟的也去掉:

PHP
1
$this->header('generator=&template=&pingback=&xmlrpc=&wlw=&atom=&rss1=&rss2=&commentReply=');

然后在评论输出代码中,加入 JS:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<script type="text/javascript">  
(function () {
    window.TypechoComment = {
        dom : function (id) {
            return document.getElementById(id);
        },
        create : function (tag, attr) {
            var el = document.createElement(tag);
            for (var key in attr) {
                el.setAttribute(key, attr[key]);
            }
            return el;
        },
        reply : function (cid, coid) {
            var comment = this.dom(cid), parent = comment.parentNode,
                response = this.dom('<?php echo $this->respondId(); ?>'),
                input = this.dom('comment-parent'),
                form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
                textarea = response.getElementsByTagName('textarea')[0];
            if (null == input) {
                input = this.create('input', {
                    'type' : 'hidden',
                    'name' : 'parent',
                    'id'   : 'comment-parent'
                });
                form.appendChild(input);
            }
            input.setAttribute('value', coid);
            if (null == this.dom('comment-form-place-holder')) {
                var holder = this.create('div', {
                    'id' : 'comment-form-place-holder'
                });
                response.parentNode.insertBefore(holder, response);
            }
            comment.appendChild(response);
            this.dom('cancel-comment-reply-link').style.display = '';
            if (null != textarea && 'text' == textarea.name) {
                textarea.focus();
            }
            return false;
        },
        cancelReply : function () {
            var response = this.dom('<?php echo $this->respondId(); ?>'),
            holder = this.dom('comment-form-place-holder'),
            input = this.dom('comment-parent');
            if (null != input) {
                input.parentNode.removeChild(input);
            }
            if (null == holder) {
                return true;
            }
            this.dom('cancel-comment-reply-link').style.display = 'none';
            holder.parentNode.insertBefore(response, holder);
            return false;
        }
    };
})();
</script>

如果不需要上述的评论框移动,只需删去comment.appendChild(response);即可。

这里使用了$this->responseId,如果没有这个值同样可以用 JS 动态获取:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<script type="text/javascript">  
(function () {
    responseId = document.getElementsByClassName("response-box")[0].id;
    window.TypechoComment = {
        dom : function (id) {
            return document.getElementById(id);
        },
        create : function (tag, attr) {
            var el = document.createElement(tag);
            for (var key in attr) {
                el.setAttribute(key, attr[key]);
            }
            return el;
        },
        reply : function (cid, coid) {
            var comment = this.dom(cid), parent = comment.parentNode,
                response = this.dom('<?php echo $this->respondId(); ?>'),
                input = this.dom('comment-parent'),
                form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
                textarea = response.getElementsByTagName('textarea')[0];
            if (null == input) {
                input = this.create('input', {
                    'type' : 'hidden',
                    'name' : 'parent',
                    'id'   : 'comment-parent'
                });
                form.appendChild(input);
            }
            input.setAttribute('value', coid);
            if (null == this.dom('comment-form-place-holder')) {
                var holder = this.create('div', {
                    'id' : 'comment-form-place-holder'
                });
                response.parentNode.insertBefore(holder, response);
            }
            comment.appendChild(response);
            this.dom('cancel-comment-reply-link').style.display = '';
            if (null != textarea && 'text' == textarea.name) {
                textarea.focus();
            }
            return false;
        },
        cancelReply : function () {
            var response = this.dom('<?php echo $this->respondId(); ?>'),
            holder = this.dom('comment-form-place-holder'),
            input = this.dom('comment-parent');
            if (null != input) {
                input.parentNode.removeChild(input);
            }
            if (null == holder) {
                return true;
            }
            this.dom('cancel-comment-reply-link').style.display = 'none';
            holder.parentNode.insertBefore(response, holder);
            return false;
        }
    };
})();
</script>

经过测试发现,即使删去了comment.appendChild(response);在取消评论时仍然会把表单内的一个<div>移动到莫名其妙的地方,很不巧 Cloudflare Turnstile 验证码会受此影响直接消失。解决方法可以是在cancelReply时重载验证码,当然最好的方法是直接彻底去掉移动评论框这个功能:

html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<script defer>  
(function () {
    responseId = document.getElementsByClassName("respond")[0].id;
    window.TypechoComment = {
        dom : function (id) {
            return document.getElementById(id);
        },
        create : function (tag, attr) {
            var el = document.createElement(tag);
            for (var key in attr) {
                el.setAttribute(key, attr[key]);
            }
            return el;
        },
        reply : function (cid, coid) {
            var comment = this.dom(cid), parent = comment.parentNode,
                response = this.dom(responseId),
                input = this.dom('comment-parent'),
                form = 'form' == response.tagName ? response : response.getElementsByTagName('form')[0],
                textarea = response.getElementsByTagName('textarea')[0];
            if (null == input) {
                input = this.create('input', {
                    'type' : 'hidden',
                    'name' : 'parent',
                    'id'   : 'comment-parent'
                });
                form.appendChild(input);
            }
            input.setAttribute('value', coid);
            this.dom('cancel-comment-reply-link').style.display = '';
            if (null != textarea && 'text' == textarea.name) {
                textarea.focus();
            }
            return false;
        },
        cancelReply : function () {
            var response = this.dom(responseId),
            input = this.dom('comment-parent');
            if (null != input) {
                input.parentNode.removeChild(input);
            }
            this.dom('cancel-comment-reply-link').style.display = 'none';
            return false;
        }
    };
})();
</script>

至此完美解决~

解决 Typecho 使用 PJAX/InstantClick 导致的评论问题

https://blog.tsinbei.com/archives/1359/

文章作者
Hsukqi Lee
发布于

2023-08-13

修改于

2023-11-19

许可协议

CC BY-NC-ND 4.0

# 建站  HTML  JavaScript  Typecho  PHP  网站

评论

昵称
邮箱
网址
2 条

飞洒

飞洒

无法评论

  回复

Hsukqi Lee

Hsukqi Lee 作者

不要回复乱码或者乱填邮箱,有自动审核

  回复