搜索全站
插件 20 分钟阅读

WordPress 评论增强插件选型:楼层、表情与 @提醒的轻量化改造

评论区不够活跃不一定缺内容。本文对比 wpDiscuz 与原生 WordPress 评论的改造路径,讲清楼层编号、@用户提醒通知、表情包的正确实现,以及 Ajax 提交与缓存/反垃圾的配合要点。

U
UFO_PF 2023-01-23 · 阅读 20 分钟
Wo 插件
阅读提示
本文约 7,943 字,预计阅读 20 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:一个楼层号 + 一组表情 + 一句 @提醒 = 一个能留住人的评论区

WordPress 自带的评论系统功能其实相当完整:嵌套回复、审核队列、黑名单、追踪引用、通知邮件都有。但它的默认形态停留在 2010 年代——没有楼层编号(用户找不到”第 5 楼 @ 了谁”)、没有表情、@某人不会有任何通知、提交后整页刷新且不能即时看到自己的发言。

很多人第一反应是装 wpDiscuz 或第三方社会化评论(多说类已死、Disqus 国内屏蔽)。这里要提醒两点:第三方评论搬家成本高(Disqus 一旦停用,历史评论全丢),重型插件对首屏有明显负担(wpDiscuz 自带大量 JS/CSS,且它默认在所有页面引入自己的 Ajax 引擎)。

本文给出的路径是:在原生评论之上做轻量增强——用 WordPress 自己的 hook 加楼层、加 @提醒通知、加一组静态表情(不需要任何 JS 库),只有确实需要 rich interactions(投票、附件、最佳回复)时才升级到 wpDiscuz。

原理分析:原生评论的四个扩展点

需求 用什么钩子/机制 说明
楼层编号 wp_list_comments() 的 callback + 静态计数器 core 不提供 floor 概念
@某人通知 wp_insert_comment / comment_post 动作 从 comment_content 里正则提取 @用户名
表情替换 get_comment_text 过滤器 把 :smile: 替换成 <img> 或 emoji
Ajax 提交 wp_ajax_* + wp_die 返回渲染好的 comment HTML
反垃圾 Akismet / Antispam Bee 必须与 Ajax 提交配合验证

三个容易被忽视的事实:

第一,楼层不等于评论 ID。 评论 ID 是全局自增且与 CPT 共享(wp_insert_comment 写入 wp_comments 表),中间可能有被删除、被审核的条目。楼层应该是”这篇文章下第几条已通过的评论”,所以必须在渲染时用计数器算,不能拿 comment_ID 直接减。

第二,wp_list_comments() 的 callback 参数决定了你能拿到什么。 传 callback 后 WordPress 会逐个调用它,你可以在外部维护一个静态变量做楼层计数。注意同一页面多个 paginate 时计数器要重置——用 $GLOBALS['mf_floor'] 并在 \wp_list_comments 之前初始化更安全。

第三,@提醒依赖 comment content 的解析时机。 必须在评论入库之后(comment_post 钩子此时 comment_ID 已存在),先 sanity check 父评论是否存在、是否接受通知,再发邮件。避免给自己发、给未审核的发。

保姆级实操

步骤 1:加楼层编号

在主题的 comments.php 里把 wp_list_comments() 改为带 callback:

$GLOBALS['mf_floor'] = array(
    'current' => 0,
    'perpage' => (int) get_option( 'comments_per_page', 10 ),
    'cpage'   => max( 1, (int) get_query_var( 'cpage', 1 ) ),
);

wp_list_comments( array(
    'style'       => 'ol',
    'short_ping'  => true,
    'avatar_size' => 40,
    'callback'    => 'mf_comment_layout',
) );

callback 函数(放在 functions.php):

function mf_comment_layout( $comment, $args, $depth ) {
    $g = &$GLOBALS['mf_floor'];
    // 倒序分页时楼层需要反向计算,这里以正序为例
    $floor = $g['current'] + 1 + ( $g['cpage'] - 1 ) * $g['perpage'];
    $g['current']++;
    ?>
    <li <?php comment_class( 'mf-comment depth-' . $depth ); ?> id="comment-<?php comment_ID(); ?>">
        <div class="mf-comment-body">
            <span class="mf-floor">#<?php echo (int) $floor; ?></span>
            <span class="mf-author"><?php comment_author_link(); ?></span>
            <time datetime="<?php comment_date( 'c' ); ?>"><?php comment_date( 'Y-m-d H:i' ); ?></time>
            <div class="mf-content"><?php comment_text(); ?></div>
            <?php
            comment_reply_link( array_merge( $args, array(
                'depth'     => $depth,
                'max_depth' => $args['max_depth'],
            ) ) );
            ?>
        </div>
    <?php // 注意:这里不闭合 <li>,WordPress 会处理嵌套
}

注意细节:comment_reply_link() 需要主题已 wp_enqueue_script( 'comment-reply' ),否则点击回复会跳 ancla #respond。

if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
    wp_enqueue_script( 'comment-reply' );
}

步骤 2:@用户提醒通知

/**
 * 从评论内容里解析 @someone,并给被 @ 的人发通知邮件
 * 依赖:后台「设置 → 讨论」已勾选相关邮件选项?否,此处自行发送,更可控
 */
add_action( 'comment_post', 'mf_notify_mentioned', 10, 2 );
function mf_notify_mentioned( $comment_id, $approved ) {
    if ( 1 !== (int) $approved ) {
        return; // 未通过审核的不发
    }
    $comment = get_comment( $comment_id );
    $post    = get_post( $comment->comment_post_ID );

    preg_match_all( '/@([A-Za-z0-9_\x{4e00}-\x{9fa5}]{2,20})/u', $comment->comment_content, $m );
    if ( empty( $m[1] ) ) {
        return;
    }
    $names = array_unique( $m[1] );

    foreach ( $names as $name ) {
        $user = get_user_by( 'login', $name );
        // 非注册用户:尝试按显示名称匹配历史评论者
        if ( ! $user ) {
            continue;
        }
        if ( (int) $user->ID === (int) $comment->user_id ) {
            continue; // 不通知自己
        }
        $subject = sprintf( '您在《%s》的评论中被 @ 了', $post->post_title );
        $body    = sprintf(
            "<p>%s 在文章中提到了您:</p><blockquote>%s</blockquote><p><a href=\"%s\">查看原文</a></p>",
            esc_html( $comment->comment_author ),
            wp_kses_post( $comment->comment_content ),
            esc_url( get_comment_link( $comment_id ) )
        );
        wp_mail( $user->user_email, $subject, $body, array( 'Content-Type: text/html; charset=UTF-8' ) );
    }
}

步骤 3:轻量表情(Emoji 方案最简单且零依赖)

WordPress 4.2+ 原生支持 emoji,编码问题由 wp_encode_emoji() 处理。给评论框加一行按钮:

add_action( 'comment_form_top', function() {
    $list = array( '????','????','????','????','????','????','????','????' );
    echo '<div class="mf-emoji-bar" role="group" aria-label="插入表情">';
    foreach ( $list as $e ) {
        printf( '<button type="button" class="mf-emoji-btn" data-emoji="%s">%s</button>',
            esc_attr( $e ), $e );
    }
    echo '</div>';
} );
document.querySelector('.mf-emoji-bar')?.addEventListener('click', function (e) {
    var btn = e.target.closest('.mf-emoji-btn');
    if (!btn) { return; }
    var ta = document.getElementById('comment');
    var emoji = btn.dataset.emoji;
    var s = ta.selectionStart, t = ta.selectionEnd;
    ta.value = ta.value.slice(0, s) + emoji + ta.value.slice(t);
    ta.selectionStart = ta.selectionEnd = s + emoji.length;
    ta.focus();
});

步骤 4:什么时候升级到 wpDiscuz

wpDiscuz(由 gVectors Team 出品,free core + 付费 addons)适合这些场景:需要投票/best answer 增加 engage rates、需要 Google/Facebook 社交登录、需要 inline file upload、需要 frontend comment moderation。如果只要楼层和表情,它太重了。 安装后注意:

  • 「wpDiscuz → Settings → Comment List」里把 Live Update 关掉(它会每 N 秒轮询,浪费请求)
  • 「Forms → General」里关掉用不到的社交登录 addon,减少加载的第三方 script
  • 确保 Akismet 等反垃圾插件仍在生效(wpDiscuz 有自己的 antispam,但不要关 Akismet)

步骤 5:Ajax 评论提交(可选,注意与安全配合)

document.getElementById('commentform')?.addEventListener('submit', function (e) {
    e.preventDefault();
    var form = e.target;
    fetch(form.getAttribute('action') || location.href, {
        method: 'POST',
        body: new FormData(form),
        credentials: 'same-origin'
    }).then(function (r) { return r.text(); })
      .then(function (html) {
          // WordPress 在成功时会返回 wp_die 的消息页,这里做简单提示即可
          location.reload();
      }).catch(function () { form.submit(); });
});

这里刻意用退化逻辑:成功则 reload,失败则原生提交。不要自己发明一套完整的 Ajax 评论,与 Akismet、审核队列、 nonce 的兼容成本远超收益。

步骤 6:性能与安全收尾

// 关闭不必要的 embeds 与 emoji JS(如果用 Emoji 方案,反而要保留 emoji script)
remove_action( 'wp_head', 'wp_oembed_add_host_js' );

// 只对文章页加载 comment-reply
add_action( 'wp_enqueue_scripts', function() {
    if ( ! ( is_singular() && comments_open() ) ) {
        wp_dequeue_script( 'comment-reply' );
    }
} );

常见踩坑

  1. 楼层编号全部错位:多半是开了「倒序分页」(设置 → 讨论 → 较高层级/最后写在最上面),此时计数器需从 总数 - (page-1)*perpage 反向递减。
  2. Ajax 提交绕过 Akismet:很多自研 Ajax 直接调 wp_new_comment(),丢失了 comment_post 之前的 pre_comment_approved 过滤链。建议直接用原生表单 POST + reload 的半 Ajax 方案。
  3. 案例:某技术博客为了让评论区”更像社区”,上线了自写 Ajax 评论,但没有对 comment_post 之后的邮件通知做去重。一次高热文章下出现了 300 多条评论且互相 @,结果触发了云服务器 25 端口的发送限制,邮件全退。更糟的是主机商把该账号临时封禁,连站点的事务性邮件(订单、找回密码)也一起停摆了两天。教训:任何”给用户发邮件”的功能都要限速 + 批量合并,且绝对不要共用业务邮件通道。
  4. comment-reply 脚本没加载:点击”回复”直接跳到锚点而不是移动表单,就是这个 script 缺失。检查是否在没有 comments_open() 的环境下被 dequeue 掉了。
  5. 嵌套深度被限制:thread_comments_depth 超过上限后 comment_reply_link() 不再出现,需要在「设置 → 讨论」调整。
  6. wpDiscuz 与其他 ajax-based 插件冲突:尤其与某些”无限滚动”插件,两者都劫持评论列表容器,导致重复渲染。
  7. Emoji 乱码:老站点的数据库字符集是 utf8(3 字节),存不下 emoji(4 字节)。必须升级到 utf8mb4:

sql
ALTER TABLE wp_comments CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

  1. 评论分页导致楼层计算错误:cpage 必须靠 get_query_var( 'cpage', 1 ) 取,且切换”最后写在最上面”时楼层的增减方向要同步反转。

延伸阅读:WordPress 评论邮件通知配置

总结

决策口径很简单:只需要楼层 + 表情 + @提醒 → 用原生评论 + 上面的三段代码(零第三方依赖、零额外 JS 库);需要社交登录、投票、最佳回复、文件上传 → 才上 wpDiscuz,并务必关掉 live update 与未使用的 addon。无论哪种,落地后请检查三件事:comment-reply 脚本只在需要处加载;Ajax 提交没有绕过 Akismet;@提醒邮件有限速且不与业务邮件共用通道。评论区是唯一能产生重复访问的地方,值得认真做,但不必用重型方案。

相关:WordPress 站内信与通知实现

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。