问题复现

在开启 Pjax 后,一般会重载一个主容器,而不会重载 Head 标签,问题是 Typecho 的评论脚本在 Head 里,如下图。

注意,红色框标注,可以看出这段脚本是根据当前页的 cid 来判断评论框所在位置。

这里补充一下,在回复评论的时候 Typecho 的评论框是会移动的,根据的就是圈注的红框,他会去找 DOM 中有没有这个元素,如果没有找到,那么就报错了,页面就会刷新,失去了整站无刷的体验。

思考过程

首先想到重载 head 标签,发现这是不可行的,因为在替换元素的一瞬间,CSS 样式被移除,网站会突然变成纯 HTML 的样子。

那么就只能从 script 标签入手了,获取到 head 第一个 script 标签,这里刚刚是第一个,选择器为 script[type],进行替换里面的内容。

尝试用正则替换,不可信,因为改完之后没有重新执行这段代码。

解决方案,使用 eval() 函数立即执行,首先要获取到原代码的内容,拷贝一份进行修改,修改的 cid 可以使用 PHP 获取当前页面的 cid。

完整代码如下,放在 post.php 末尾即可

(function () {
            const commentFunction = document.querySelector('head').querySelector('script[type]')
            const innerHTML = commentFunction.innerHTML
            if (innerHTML.match(/this.dom\('respond-.*?'\)/ig)) {
                const after = innerHTML.replace(/this.dom\('respond-.*?'\)/ig, "this.dom('respond-post-<?php $this->cid() ?>')")
                eval(after)
            }

        })()

遗留问题

没有考虑到首页是不存在这个标签的,因为还没有进入文章页,所以这时候直接插入这段代码就行了。

else {
              const script = document.createElement('script')
              script.innerHTML = `
(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('respond-post-<?php $this->cid() ?>'), 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('respond-post-<?php $this->cid() ?>'),
            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;
        }
    };
})();
`
              document.head.insertBefore(script, commentFunction)
              eval(script.innerHTML)
            }

后续

后来在 Chrome 中遇到了 Maximum call stack size exceeded 的问题,简单地说就是堆栈溢出了。因为平时使用 Cent Browser 开发所以没怎么注意到这个问题。那就只能再改呗。那只能改的更傻瓜一点了。

(
    function () {
      const reply = function (cid, coid) {
        var comment = this.dom(cid), parent = comment.parentNode,
          <?php if($this->is('post')): ?>
          response = this.dom('respond-post-<?php $this->cid() ?>'), input = this.dom('comment-parent'),
          <?php elseif ($this->is('page')): ?>
          response = this.dom('respond-page-<?php $this->cid() ?>'), input = this.dom('comment-parent'),
          <?php endif; ?>
          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;
      }
      const cancelReply = function () {
        <?php if ($this->is('post')): ?>
        var response = this.dom('respond-post-<?php $this->cid() ?>'),
          <?php elseif ($this->is('page')): ?>
        var
        response = this.dom('respond-page-<?php $this->cid() ?>'),
        <?php endif ?>
          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;
      }
      if (window.TypechoComment) {
        window.TypechoComment.reply = reply
        window.TypechoComment.cancelReply = cancelReply
      } else {
        (() => {
          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('respond-post-<?php $this->cid() ?>'), 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('respond-post-<?php $this->cid() ?>'),
                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;
            }
          };
        })();
      }
    })();

今天遇到的问题 浏览器在执行 IIFE 函数时,浏览器会在头部 head 标签加入一个 <script type="text/javascript" id> 的标签,所在 IIFE 内部使用 script[type] 可能会捕获到自身,并非自己理想状态的 null

补充说明

eval() 导致的堆栈溢出可以采用 setTimeOut() 解决。

完整代码:https://github.com/Innei/Typecho-Theme-Paul/blob/master/comments.php