WordPress 的钩子(Hook)机制是插件和主题开发的基石,但很多开发者都遇到过“代码写了却不生效”的问题。原因往往不是代码本身,而是钩子触发时机不对。比如在 init 里判断 is_single(),或者在 wp_head 里注册文章类型,都会因为时机过早或过晚而失败。
这篇文章把常用 Action Hook 按标准请求的主线顺序整理成一张表,并补充后台、文章保存、REST API 和 AJAX 等独立流程的关键差异。你可以把它当作速查手册,也可以用来排查钩子顺序问题。
一、标准前端请求:Hook 触发顺序总表
从请求进入到页面渲染完成,核心顺序如下(只列常用、有实际介入价值的钩子):
| 顺序 | Hook | 典型用途 |
|---|---|---|
| 1 | muplugins_loaded | Must-use 插件已加载,最早的可介入点 |
| 2 | plugins_loaded | 所有活动插件已加载,检查其他插件是否存在 |
| 3 | setup_theme | 主题加载之前 |
| 4 | after_setup_theme | 主题 functions.php 已加载,注册主题支持、导航菜单 |
| 5 | init | 注册文章类型、分类法、短代码;当前用户已认证 |
| 6 | widgets_init | 注册侧边栏和小工具 |
| 7 | wp_loaded | WordPress、插件、主题全部加载完毕 |
| 8 | parse_request | URL 解析,可介入请求处理 |
| 9 | send_headers | 发送 HTTP 头之前 |
| 10 | parse_query | 查询变量已设置,可修改 WP_Query 对象 |
| 11 | pre_get_posts | 查询执行之前,修改查询参数的最后关键点 |
| 12 | wp | 主查询已完成,条件判断(is_single() 等)可用 |
| 13 | template_redirect | 模板选择之前,重定向、访问控制的最后机会 |
| 14 | template_include(Filter) | 覆盖要加载的模板文件路径 |
| 15 | get_header | 加载 header.php 之前 |
| 16 | wp_enqueue_scripts | 前端样式/脚本入队 |
| 17 | wp_head | 输出 </head> 之前,插入 meta、内联样式等 |
| 18 | loop_start → the_post → loop_end | 主循环期间 |
| 19 | get_sidebar / get_footer | 加载侧边栏 / 页脚之前 |
| 20 | wp_footer | 输出 </body> 之前,插入统计代码、脚本 |
这条主线的“脊柱”可以压缩为:
text
muplugins_loaded → plugins_loaded → setup_theme → after_setup_theme → init → widgets_init → wp_loaded → template_redirect → wp_head → wp_footer
其中 wp 钩子位于主查询完成之后、template_redirect 之前。它的价值在于:此时查询数据已经就绪,但模板尚未确定,适合基于查询结果执行逻辑。template_redirect 之后才会进入模板文件选择与加载阶段;如果你需要更晚的模板加载节点,还可以关注 wp_before_include_template(WordPress 6.9.0 引入),它在模板文件真正被 include 之前触发,晚于 template_redirect。
二、后台管理请求的 Hook 顺序
后台请求同样经过 init、wp_loaded,但随后进入独立的 admin 流程。关键顺序:
text
after_setup_theme → init → wp_loaded → admin_menu → admin_init → admin_enqueue_scripts
这里有一个常见的顺序“陷阱”:从字面看 admin_init 似乎应该最早,但在核心实际执行中,admin_menu 通常先于 admin_init 触发。因此:
admin_menu:注册后台菜单页面的正确位置。admin_init:注册设置字段、执行后台权限检查。admin_enqueue_scripts:后台资源入队,晚于前两者。
如果你要添加后台菜单页,不要挂在 admin_init 上;如果你要注册设置项,也不要挂在 admin_menu 上。各司其职,才能避免顺序问题。
三、文章保存(创建/更新)的 Hook 顺序
文章保存由 wp_insert_post() 驱动,wp_update_post() 内部也会调用它。核心顺序:
text
pre_post_update(仅更新时) → wp_insert_post_data(Filter) → save_post → wp_insert_post → transition_post_status
要点:
wp_insert_post_data是 Filter,在数据写入数据库之前触发,适合在保存前修改数据。save_post和wp_insert_post在wp_insert_post()的末尾几乎同时触发,参数相同,save_post先执行。transition_post_status的时序因文章类型而异:常规文章可能在save_post之后触发;自定义文章类型则可能在save_post之前触发,需要额外留意。
四、REST API 与 AJAX:独立生命周期
这两类请求不经过标准模板加载流程,template_redirect、wp_head、wp_footer 都不会触发。
REST API 的关键顺序:
text
rest_api_init(注册路由) → rest_pre_dispatch → 认证 → 路由匹配 → rest_prepare_{$post_type} → rest_insert_{$post_type}(保存类操作)
rest_api_init 是注册 REST 路由的专用入口,不要用 init 代替。
AJAX(admin-ajax.php)的关键顺序:
text
admin_init → wp_loaded → wp_ajax_{$action}(或 wp_ajax_nopriv_{$action})
wp_ajax_{$action} 在 admin_init 和 wp_loaded 之后触发,但在 template_redirect 之前(因为根本不会走到模板加载)。
五、几个容易混淆的顺序对照
| 对比 | 谁先谁后 | 说明 |
|---|---|---|
init vs wp_loaded | init 先 | init 时用户已认证,但主查询尚未解析 |
wp vs template_redirect | wp 先 | wp 时查询已就绪,template_redirect 时还未选模板 |
admin_menu vs admin_init | admin_menu 先 | 与字面直觉相反,admin_init 实际在后 |
save_post vs wp_insert_post | save_post 先 | 两者在 wp_insert_post() 末尾紧邻触发 |
rest_api_init vs init | init 先 | rest_api_init 是 REST 请求专属,晚于全局 init |
六、速查总结
记住三条主线,大部分钩子问题都能定位:
- 前端主线:
init→wp_loaded→wp→template_redirect→wp_head→wp_footer - 后台主线:
init→wp_loaded→admin_menu→admin_init→admin_enqueue_scripts - 保存主线:
wp_insert_post_data→save_post→wp_insert_post→transition_post_status
最后提醒一句:钩子顺序虽然重要,但更重要的是在正确的钩子里做正确的事。注册文章类型用 init,修改主查询用 pre_get_posts,拦截模板用 template_redirect,输出头部内容用 wp_head。把职责放对,顺序问题自然就少了。