插件开发与 Hook 体系
这一课你将能做到
- 从零写出规范插件骨架:主文件头注释、激活/停用/卸载钩子一个不缺
- 区分 action 与 filter,用优先级和参数个数精准控制回调的执行顺序
- 在文章保存、请求初始化等真实时机挂载自己的逻辑(如保存时自动打标签)
为什么要学
WordPress 的 80% 二次开发是通过 Hook(钩子)完成的:WP 在执行流程的关键节点「喊一声」,你的插件、主题响应它。不改核心、不碰模板,就能改变全站行为——这是 WP 生态繁荣的技术根基,也是面试必考题。
人话:把 WP 想象成流水线,Hook 是流水线上预留的插口。action 插口是「到这一步了,你去干件事」(发邮件、写日志);filter 插口是「这个数据交给你加工,加工完必须还回来」(改标题、追加正文)。分不清这两点,插件开发寸步难行。
先建立图像
一次请求的执行顺序与主要插口:
请求进入
→ 加载 mu-plugins(必用插件,最先)
→ 按字母序加载 plugins ← 你插件的 add_action 在这里注册
→ 加载主题 functions.php
→ plugins_loaded
→ init ← 常用:注册 CPT、_taxonomy
→ wp_loaded → 解析主查询
→ template_redirect / the_content(filter) / wp_enqueue_scripts
→ 渲染模板 → wp_footer → 输出
注册发生在「加载期」,执行发生在「运行期」——所以你可以在 init 之前安全地 add_action,但很多 WP 函数要等 init 之后才能用。
跟着做
步骤 1:插件骨架
wp-content/plugins/qrocmq-toolkit/ 下新建主文件 qrocmq-toolkit.php:
<?php
/**
* Plugin Name: QROCMQ Toolkit
* Description: 课程站工具:保存文章自动追加来源说明。
* Version: 1.0.0
* Requires at least: 6.5
* Requires PHP: 8.2
* Author: qrocmq
* Text Domain: qrocmq-toolkit
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // 防止直接访问
}
保存后后台「插件」页应出现它——头注释就是插件的身份证明。ABSPATH 检查是安全底线。
步骤 2:第一个 action
在骨架下方追加:
add_action( 'admin_notices', function () {
echo '<div class="notice notice-info"><p>QROCMQ Toolkit 已启用</p></div>';
} );
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'qrocmq-toolkit',
plugin_dir_url( __FILE__ ) . 'assets/main.css',
array(),
'1.0.0'
);
} );
激活插件,后台顶部出现提示条——你的第一段代码已进入 WP 执行流程。
步骤 3:filter 实战:改内容必须还数据
add_filter( 'the_content', function ( string $content ): string {
if ( is_singular( 'post' ) ) {
$content .= '<p class="source-note">本文首发于 QROCMQ 课程站</p>';
}
return $content; // filter 的铁律:必须 return
}, 20 ); // 优先级 20:晚于默认的 10 执行
add_filter( 'the_title', function ( string $title, int $post_id ): string {
return is_admin() ? $title : $title . '|课程笔记';
}, 10, 2 ); // 第三个参数:接收 2 个参数
注意 add_filter 第 4 参:默认 1,钩子传几个参数就声明几个,少声明会导致回调收不到数据。
步骤 4:生命周期钩子:激活建数据,卸载要清理
register_activation_hook( __FILE__, function () {
add_option( 'qrocmq_toolkit_version', '1.0.0' );
update_option( 'qrocmq_toolkit_activated_at', time() );
} );
register_deactivation_hook( __FILE__, function () {
wp_cache_flush(); // 停用时收尾
} );
卸载清理单独放 uninstall.php(删除插件时自动执行):
<?php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'qrocmq_toolkit_version' );
delete_option( 'qrocmq_toolkit_activated_at' );
步骤 5:在文章保存时做事
add_action( 'save_post', function ( int $post_id, WP_Post $post, bool $update ): void {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return; // 跳过修订与自动保存
}
if ( 'post' !== $post->post_type ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return; // 能力检查,防越权
}
// 示例:给新文章自动加默认标签
wp_set_post_tags( $post_id, '课程', true );
}, 10, 3 );
步骤 6:自定义钩子,让别人扩展你
// 你的插件里:发布数据前给外部一个修改机会
$data = apply_filters( 'qrocmq_course_payload', $data, $course_id );
do_action( 'qrocmq_after_course_saved', $course_id, $data );
成熟插件(WooCommerce 为代表)整个就是「自定义钩子的集合」。
讲清楚要点
- action 与 filter 的唯一本质区别:filter 必须返回值(数据进出),action 只执行(有去无回)。
do_action触发,apply_filters触发并传递。 - 优先级决定顺序,参数个数决定传参:同为 10 按注册先后执行;回调收不到参数,99% 是第 4 参没写。
- 加载顺序是纪律:muplugins → plugins → theme functions.php → init。写代码前先问「我现在这个时机,哪些函数可用」。
- 防冲突三板斧:函数与常量加前缀(qrocmq_)、用命名空间、类的静态属性存状态。
- 数据落地用 Options/Settings API:配置项
add_option/update_option/get_option,不要自己发明存储。
你可能会懵的地方
-
同一钩子挂了多个回调,顺序是什么? 优先级数字小者先执行,数字相同按注册顺序。用 Query Monitor 的 Hooks 面板能直接看到全部注册。
-
为什么我的 filter 把页面改空了? 回调漏了
return。filter 链上你的返回值是下一个环节的输入,返回 null 内容就没了。 -
插件和 functions.php 谁先加载? 插件全部先于主题。所以主题里可以依赖插件提供的函数吗?不行——插件之间也一样,除非挂在
plugins_loaded之后再判断。 -
改了插件代码不生效? 依次排查:对象缓存/页面缓存、OPcache、看的是不是生产目录。本地先关缓存再改。
-
删除插件时 uninstall 钩子没执行? 必须先「停用」再「删除」才会走卸载流程;且逻辑要写进 uninstall.php 而不是 register_uninstall_hook(后者只支持静态回调,官方推荐文件方式)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| filter 回调不 return | 数据链断裂,内容变空 | 每条分支都 return |
| add_action( 'init' ) 里 echo | 输出时机过早,进缓冲区前 | 用 admin_notices / wp_footer 等展示钩子 |
| 函数名 add_meta_box() 裸命名 | 与核心或其他插件致命冲突 | 前缀 qrocmq_ 或命名空间 |
| save_post 不排除 revision | 每次自动保存都触发一遍 | wp_is_post_revision / autosave 判断 |
| 在文件顶层调用 get_option() | 加载期可能不可用 | 包在 init/plugins_loaded 回调里 |
卡点急救
- 激活即白屏:主文件语法错或致命错误,
wp-content/debug.log看堆栈;可先把插件目录改名使其不加载再修。 - 钩子像没执行:拼写核对(save_post 不是 save_post_)、时机核对(早于注册)、参数个数核对;用 Query Monitor → Hooks 确认注册是否在列。
- 卸载后数据残留:把清理逻辑迁移到 uninstall.php,并停用→删除走完整流程验证一次。
面试常问
-
action 和 filter 的区别? action 在特定时机执行动作,无返回值要求;filter 接收数据加工后必须返回,可链式串联。API 上对应 do_action/apply_filters 与 add_action/add_filter。
-
一次页面请求中,插件与主题的加载顺序? mu-plugins → 插件(按文件名字母序)→ 主题 functions.php → init。因此主题可以安全覆盖插件的样式输出,但函数依赖要晚于 plugins_loaded 检查。
-
需求:文章发布时推送一条消息,怎么实现? 挂
transition_post_status(比 save_post 更精准:能判断从旧状态到 publish),排除修订与自动保存、做能力检查后调用推送;推送失败要容错,不阻塞保存。
自测三问
- add_filter( 'the_title', $cb, 10, 2 ) 的 10 和 2 分别控制什么?
- 插件激活、停用、卸载三个阶段分别用什么 API 处理?
- save_post 钩子里必须排除哪两种情况?为什么?
小结一句话
插件 = 头注释 + 钩子注册 + 生命周期管理;action 管做事、filter 管加工,优先级与参数个数是你控制 WP 执行流程的两只手。