搜索全站
安全防护 16 分钟阅读

WordPress 钩子机制详解:Action 与 Filter 的区别与实战用法

不懂钩子,改 WordPress 就只能改核心文件。本文讲清 Action(动作)与 Filter(过滤器)的本质区别、执行时机、优先级与参数个数的规则,给出 20 个最常用的钩子速查表,以及调试钩子顺序的实用方法。

U
UFO_PF 2018-06-11 · 阅读 16 分钟
Wo 安全防护
阅读提示
本文约 6,357 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:为什么不能改核心文件

想让 WordPress 做点自定义的事,最直觉的做法是打开 wp-includes 里某个文件改两行。但这个做法的代价是:下次 WordPress 自动更新,你的改动全部被覆盖,而且改核心是安全大忌。

正确做法是钩子(Hooks):WordPress 在执行流程里预埋了大量”插槽”,你把自己的函数挂上去,WordPress 跑到那里就会执行它。这也是插件和主题扩展 WordPress 的唯一正道。

原理分析:Action 和 Filter 到底差在哪

WordPress 有两种钩子,本质区别只有一句话:

  • Action(动作):在某个时间点做一件事,不关心返回值。do_action() 触发。
  • Filter(过滤器):修改一份数据并必须返回。apply_filters() 触发。

源码层面(简化):

// Action
function do_action( $hook_name, ...$arg ) {
    foreach ( $GLOBALS['wp_filter'][$hook_name]->callbacks as $priority => $cbs ) {
        foreach ( $cbs as $cb ) {
            call_user_func_array( $cb['function'], $arg );   // 不接收返回值
        }
    }
}

// Filter
function apply_filters( $hook_name, $value, ...$arg ) {
    foreach ( $GLOBALS['wp_filter'][$hook_name]->callbacks as $priority => $cbs ) {
        foreach ( $cbs as $cb ) {
            $value = call_user_func_array( $cb['function'], array( $value, ...$arg ) );  // 必须 return
        }
    }
    return $value;
}

Filter 忘记 return 是最经典的错误——数据被清空,前台一片空白。

注册函数:

add_action( $hook, $callback, $priority = 10, $accepted_args = 1 );
add_filter( $hook, $callback, $priority = 10, $accepted_args = 1 );
  • priority(优先级):默认 10,数字越小越早执行。相同优先级按注册顺序。
  • accepted_args:回调函数接收几个参数。默认是 1,如果需要第二个参数必须显式声明,否则会报 “Too few arguments”。
// 错误:需要 2 个参数却声明默认 1 个
add_filter( 'the_title', function( $title ) { ... } );

// 正确
add_filter( 'the_title', function( $title, $post_id ) {
    return $title;
}, 10, 2 );

保姆级实操

一、20 个最常用钩子速查

钩子 类型 触发时机 典型用途
init Action WordPress 加载完、发送头之前 注册 CPT、分类法
wp_loaded Action 完全加载后 重定向判断
template_redirect Action 模板选择前 权限跳转、404 处理
wp_head Action 前台 <head> 输出 meta、统计代码
wp_footer Action 前台底部 输出脚本
wp_enqueue_scripts Action 前台加载资源 注册 CSS/JS
admin_enqueue_scripts Action 后台加载资源 后台 CSS/JS
after_setup_theme Action 主题初始化 add_theme_support、注册菜单
widgets_init Action 小工具初始化 注册侧边栏、小工具
save_post Action 保存文章后 清缓存、通知
publish_post Action 文章发布时 推送、订阅通知
wp_login / wp_logout Action 登录/登出 记录日志
rest_api_init Action REST 初始化 注册自定义端点
pre_get_posts Action 主查询前 修改查询条件
the_content Filter 输出正文前 追加内容、替换
the_title Filter 输出标题前 标题加工
the_excerpt Filter 输出摘要前 摘要长度
excerpt_length Filter 摘要字数 改 55 为其他值
the_permalink Filter 输出链接前 链接处理
wp_nav_menu_items Filter 输出菜单前 菜单追加项
body_class Filter body 的 class 加自定义 class
manage_edit-post_columns Filter 后台列表列 自定义列

二、Action 实战:文章发布后推送通知

add_action( 'publish_post', 'mmf_notify_on_publish', 10, 2 );
function mmf_notify_on_publish( $post_id, $post ) {
    // 避免自动保存和修订触发
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( wp_is_post_revision( $post_id ) ) {
        return;
    }

    $data = array(
        'title' => $post->post_title,
        'url'   => get_permalink( $post_id ),
    );

    wp_remote_post( 'https://your-webhook.example.com/notify', array(
        'body'    => json_encode( $data ),
        'headers' => array( 'Content-Type' => 'application/json' ),
        'timeout' => 5,
        'blocking' => false,     // 非阻塞,不拖慢发布
    ) );
}

注意 DOING_AUTOSAVE 和 wp_is_post_revision() 这两个判断——所有 save_post 系列钩子都必须做,否则自动保存时会触发一堆副作用。

三、Filter 实战:正文末尾追加内容

add_filter( 'the_content', 'mmf_append_cta' );
function mmf_append_cta( $content ) {
    // 只在单篇文章的主循环里追加
    if ( ! is_single() || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }
    $cta = '<div class="post-cta"><p>如果本文对你有帮助,欢迎收藏本站。</p></div>';
    return $content . $cta;   // 必须 return
}

in_the_loop() + is_main_query() 判断很重要,否则侧边栏的相关文章输出也会被追加内容。

四、自定义钩子(让你的主题/插件可被扩展)

// 定义 Action
do_action( 'mmf_before_post_content', get_the_ID() );

// 定义 Filter
$html = apply_filters( 'mmf_post_cta_html', $html, get_the_ID() );

这样别人(或你自己)不用改你的代码就能扩展:

add_action( 'mmf_before_post_content', function( $post_id ) {
    echo '<p>赞助内容</p>';
} );

五、移除他人的钩子

// 移除函数
remove_action( 'wp_head', 'wp_generator' );          // 移除版本号
remove_filter( 'the_content', 'wpautop' );           // 移除自动段落

// 移除类方法(需要拿到实例)
global $my_plugin;
remove_action( 'wp_footer', array( $my_plugin, 'output' ) );

// 移除匿名函数(闭包)—— 无法直接移除!

匿名函数(闭包)无法用 remove_action 移除,这是个真实痛点:

// 无法移除
add_action( 'wp_head', function() { echo 'x'; } );

// 可移除
function mmf_my_head() { echo 'x'; }
add_action( 'wp_head', 'mmf_my_head' );
remove_action( 'wp_head', 'mmf_my_head' );

所以写可能被别人覆盖的钩子时,用命名函数而不是闭包。

六、移除时必须注意优先级一致

add_action( 'wp_head', 'mmf_func', 20 );

// 移除时必须指定相同优先级
remove_action( 'wp_head', 'mmf_func', 20 );    // 正确
remove_action( 'wp_head', 'mmf_func' );        // 错误,移除不掉

七、调试钩子执行顺序

add_action( 'all', function( $tag ) {
    static $count = 0;
    if ( $count++ < 500 ) {
        error_log( sprintf( '%03d %s', $count, $tag ) );
    }
});

这个 all 钩子会记录所有触发的钩子,写进 debug.log,是定位”我的代码到底跑没跑”的神器。

看某个钩子上挂了什么:

add_action( 'wp_footer', function() {
    if ( ! current_user_can( 'manage_options' ) ) return;
    global $wp_filter;
    if ( isset( $wp_filter['the_content'] ) ) {
        echo '<pre>';
        foreach ( $wp_filter['the_content']->callbacks as $priority => $cbs ) {
            foreach ( $cbs as $cb ) {
                echo $priority . ' => ' . ( is_string($cb['function']) ? $cb['function'] : 'closure' ) . "\n";
            }
        }
        echo '</pre>';
    }
}, 999 );

常见踩坑与失败案例

  1. 案例一:add_filter('the_content', ...) 里忘了 return,前台所有文章正文全空。这是 Filter 最经典的事故。
  2. 案例二:add_filter('the_title', function($title, $id){...}) 报错 “Too few arguments”。修正:add_filter('the_title', function($title,$id){...}, 10, 2)。
  3. remove_action 移除不掉:优先级不一致,或者对方用的是闭包,或者移除代码执行得比注册还早(要在对方注册之后移除,通常挂到 init 或优先级更大的钩子上)。
  4. 案例三:save_post 里写了发邮件逻辑,结果每次自动保存都发一封。加了 DOING_AUTOSAVE 判断才解决。
  5. pre_get_posts 影响了后台:忘了 is_admin() 判断,导致后台文章列表只剩 5 篇。
  6. 案例四:the_content 过滤器里追加了内容,结果 RSS 里也有、摘要里也有、侧边栏相关文章里也有。加了 is_single() && is_main_query() && in_the_loop() 三重判断才正常。
  7. 优先级设成 0 或负数:虽然合法,但会和 WordPress 核心的执行顺序冲突,建议用 1~999。
  8. 在 init 之前调用 add_action:在插件文件顶层直接写 add_action 是可以的(WordPress 加载插件时 wp_filter 已初始化),但如果在 functions.php 顶层调用某些还没定义的函数就会报错。

总结

钩子就一条核心区分:Action 做事不 return,Filter 改数据必须 return。参数上记住 accepted_args 默认 1,要多传参必须显式声明。save_post 系列必须加 DOING_AUTOSAVE 和 wp_is_post_revision() 判断;the_content 类过滤器必须加 is_main_query() / in_the_loop() 判断避免污染侧边栏和 RSS。写可能被覆盖的钩子用命名函数(闭包无法 remove)。调试用 all 钩子打日志、打印 $wp_filter 看挂载情况——这两招能解决九成的”我的代码没生效”问题。

U
UFO_PF作者

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

留下你的想法

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