`register_activation_hook` 里建表成功,前台却 404:我花了三小时才意识到 `flush_rewrite_rules` 的"时差谋杀"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 7 浏览 0 回复

上周给客户交付一个带自定义文章类型(CPT)的会员插件,本地一切正常,打包上传后激活——表建了、选项写了、菜单出了,前台访问 `/member/card/123` 直接 404。重启 PHP、清缓存、甚至重装插件,幽灵 404 纹丝不动。

最后发现凶手是 register_activation_hook 里那句 flush_rewrite_rules() 的调用时机。写出来给同样被坑过的兄弟提个醒。

案发现场还原

插件主文件里的激活逻辑大概长这样:

register_activation_hook( __FILE__, function() {
    // 1. 建表
    myplugin_create_tables();
    
    // 2. 注册 CPT(其实这里写错了位置,但先按下不表)
    myplugin_register_member_cpt();
    
    // 3. 刷新重写规则
    flush_rewrite_rules();
    
    // 4. 写默认配置
    update_option( 'myplugin_version', '1.0.0' );
} );

看起来没毛病?问题出在 第 2 步和第 3 步的"伪执行"

陷阱一:register_post_type 在激活钩子内"注册即销毁"

register_activation_hook 是在插件文件被加载的 单次请求 内执行的。你在这里调用 register_post_type(),WordPress 确实会把它写进全局变量,但这个请求结束后,下次正常访问前台时,CPT 的注册要靠插件日常加载逻辑——也就是 init 钩子上的回调。

所以激活时 flush_rewrite_rules() 刷进去的规则集,根本不包含你的 CPT 路由,因为你还没在 init 上挂注册函数呢!它刷的是旧规则,或者更准确说,是"当前这次请求里临时存在、下次请求就不见了"的残缺规则。

陷阱二:flush_rewrite_rules() 的"假刷新"

更坑的是,flush_rewrite_rules() 默认参数是 true(硬刷新写入数据库),但它在激活钩子内部执行时,由于前面说的 CPT 未真正注册,写入的 rewrite_rules 选项值是不带你的自定义端点的

然后你前台访问,WordPress 从数据库读出这套"干净"的规则,404 是必然的。

我的修复方案:把刷新推迟到"注册完成后"的第二次请求

改完后的结构分成三块:

// === 1. 日常加载:在 init 上注册 CPT ===
add_action( 'init', 'myplugin_register_member_cpt', 5 ); // 优先级 5,早点跑

function myplugin_register_member_cpt() {
    register_post_type( 'member_card', [
        'public'      => true,
        'has_archive' => true,
        'rewrite'     => [ 'slug' => 'member/card' ],
        // ... 其他参数
    ] );
}

// === 2. 激活钩子:只负责"标记需要刷新",不直接刷 ===
register_activation_hook( __FILE__, function() {
    myplugin_create_tables();
    update_option( 'myplugin_version', '1.0.0' );
    
    // 关键:设置一个标志位,而不是直接 flush
    update_option( 'myplugin_flush_rewrite_flag', 1 );
} );

// === 3. 在 init 末尾检查标志位,此时 CPT 已注册,再刷才有效 ===
add_action( 'init', 'myplugin_maybe_flush_rewrites', 99 ); // 优先级 99,等所有注册跑完

function myplugin_maybe_flush_rewrites() {
    if ( get_option( 'myplugin_flush_rewrite_flag' ) ) {
        flush_rewrite_rules();
        delete_option( 'myplugin_flush_rewrite_flag' );
    }
}

核心逻辑:激活时只插旗子,下次 init 跑完(CPT 已稳)再真正刷新。这样数据库里写入的规则集才是完整的。

额外踩的第三个坑:多站点环境下 switch_to_blog 的副作用

客户环境是子目录多站点(/site1//site2/),我在网络激活时用了 switch_to_blog 循环建表,结果 flush_rewrite_rules() 在切回主站点后行为异常——它把子站点的规则写到了主站点,或者干脆没写。

最后把刷新逻辑完全剥离出网络激活循环,每个子站点靠访问时的 init 钩子自刷新,才彻底干净。

总结:三条铁律

1. 激活钩子内绝不直接 flush_rewrite_rules,除非你确定所有自定义路由已注册——而它们通常要在 init 才注册。

2. 用"延迟标志位"模式:激活时 update_option 插旗,init 末尾检测并执行刷新。

3. 多站点网络激活时,flush_rewrite_rulesswitch_to_blog 不要混用,让每个站点按需自刷新。

这坑我踩了三小时,日志里没有错误、数据库里表也在、选项也写了,就是 404。最难查的 bug 从来不是报错,是"一切正常但结果不对"。

评论0
回复 · 0
还没有回复
微信客服 微信客服