本文约 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 );
常见踩坑与失败案例
- 案例一:
add_filter('the_content', ...)里忘了return,前台所有文章正文全空。这是 Filter 最经典的事故。 - 案例二:
add_filter('the_title', function($title, $id){...})报错 “Too few arguments”。修正:add_filter('the_title', function($title,$id){...}, 10, 2)。 remove_action移除不掉:优先级不一致,或者对方用的是闭包,或者移除代码执行得比注册还早(要在对方注册之后移除,通常挂到init或优先级更大的钩子上)。- 案例三:
save_post里写了发邮件逻辑,结果每次自动保存都发一封。加了DOING_AUTOSAVE判断才解决。 pre_get_posts影响了后台:忘了is_admin()判断,导致后台文章列表只剩 5 篇。- 案例四:
the_content过滤器里追加了内容,结果 RSS 里也有、摘要里也有、侧边栏相关文章里也有。加了is_single() && is_main_query() && in_the_loop()三重判断才正常。 - 优先级设成 0 或负数:虽然合法,但会和 WordPress 核心的执行顺序冲突,建议用 1~999。
- 在
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 看挂载情况——这两招能解决九成的”我的代码没生效”问题。
