Zsens Admin 插件模板继承中的"路径幽灵":当 `locate_template` 返回子主题路径后,我的 `wp_enqueue_style` 还在往插件目录死磕

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

上周帮一个兄弟排查问题,现象很诡异:子主题成功覆写了插件的仪表盘模板,但页面上所有自定义组件的样式全丢了。F12 一看,CSS 请求 404,路径居然还是 wp-content/plugins/zsens-admin/assets/css/...,可实际文件明明在子主题的 zsens-admin/overrides/assets/css/ 里躺着。

问题的根子出在"模板继承"和"资源发布"是两个独立通道,很多人(包括我早期)潜意识里把覆写模板当成了"整个插件视图层搬家",其实 WordPress 的模板定位机制根本不会替你通知资源加载逻辑。

先还原现场。插件里原来的资源注册大概长这样:

// 插件核心:注册仪表盘样式
add_action( 'admin_enqueue_scripts', function ( $hook ) {
    if ( 'toplevel_page_zsens-dashboard' !== $hook ) {
        return;
    }
    wp_enqueue_style(
        'zsens-dashboard',
        plugin_dir_url( __FILE__ ) . 'assets/css/dashboard.css',
        [],
        ZSENS_VERSION
    );
} );

子主题这边,locate_template( [ 'zsens-admin/dashboard.php' ] ) 确实能命中 child-theme/zsens-admin/dashboard.php,模板继承通路是通的。但资源呢?plugin_dir_url( __FILE__ ) 永远指向插件目录,它不知道、也不关心模板被谁覆写了。

我当时的思路是:让资源路径"追随"模板的宿主环境。模板在哪被找到的,资源就从哪加载。但这里有个坑——你不能在 enqueue 阶段再去跑一次文件系统探测,否则每个后台页面都多两次 IO,慢得要死。

最后用的方案是"注册时预埋锚点,运行时惰性解析":

// 插件核心:改为注册"逻辑句柄",不直接写死 URL
add_action( 'admin_enqueue_scripts', function ( $hook ) {
    if ( 'toplevel_page_zsens-dashboard' !== $hook ) {
        return;
    }
    
    // 只注册句柄,URL 延迟到 _doing_it_wrong 之前的那一刻
    wp_register_style( 'zsens-dashboard', false, [], ZSENS_VERSION );
    wp_enqueue_style( 'zsens-dashboard' );
} );

// 在 styles_loader_src 过滤器里做"宿主感知"替换
add_filter( 'style_loader_src', function ( $src, $handle ) {
    if ( 'zsens-dashboard' !== $handle ) {
        return $src;
    }
    
    // 关键:复用模板定位结果,而不是重新探测
    $template_path = locate_template( 'zsens-admin/dashboard.php' );
    
    if ( $template_path ) {
        // 模板被子主题/父主题覆写了,资源跟过去
        $theme_uri  = get_stylesheet_directory_uri(); // 子主题优先
        $theme_path = get_stylesheet_directory();
        
        // 把文件系统路径转成 URI:找到模板相对于主题根的位置
        $relative   = str_replace( trailingslashit( $theme_path ), '', dirname( $template_path ) );
        
        // 检查子主题是否同时提供了同名资源
        $override_css = trailingslashit( $theme_path ) . $relative . '/assets/css/dashboard.css';
        
        if ( file_exists( $override_css ) ) {
            return trailingslashit( $theme_uri ) . $relative . '/assets/css/dashboard.css';
        }
    }
    
    // 回退:没人覆写资源,用插件自带的
    return plugin_dir_url( ZSENS_MAIN_FILE ) . 'assets/css/dashboard.css';
    
}, 10, 2 );

这个写法有几个刻意的设计:

1. 不重复探测
locate_template 的结果 WordPress 内部有缓存(虽然是非持久化的,但同一次请求内不会重复查磁盘),我直接复用这个结果,而不是自己再写一套 file_exists 轮询。

2. 子主题优先于父主题
get_stylesheet_directory_uri()get_template_directory_uri() 的区别很多人混用。如果模板是被父主题覆写的,资源也应该跟父主题走——但我这里用了 locate_template 返回的绝对路径反推,所以实际走的是"模板所在主题",而不是硬编码子主题。

3. 资源覆写是可选的
子主题可以只覆写模板不碰资源,也可以两者都覆写。上面的逻辑会优先检查同目录下有没有对应的 CSS,没有就回退到插件默认。这样不会逼用户"全包全揽"。

但这里还藏着一个更隐蔽的坑:style_loader_src 过滤器在 WP 的脚本加载流程里执行时机比较晚,如果你的模板里用了 wp_add_inline_style( 'zsens-dashboard', ... ),而句柄注册时 srcfalse,inline 样式会挂不上去——因为 WP 认为这个句柄"没有实际文件",inline 的附着点不存在。

解决方法是把 inline 样式的挂载也推迟到同一个过滤器的下游,或者改用 wp_register_style 时给一个占位 URL(比如 #zsens-placeholder),然后在过滤器里替换掉。我选了后者,更稳:

wp_register_style( 'zsens-dashboard', '#zsens-placeholder', [], ZSENS_VERSION );

过滤器里最后 return 真实 URL 之前,把占位符冲掉就行。

这套机制跑通之后,我把资源类型扩展到 JS、图片、甚至 Web Font,逻辑是一样的。唯一要注意的是:如果子主题覆写了资源,版本号建议子主题自己维护,或者走文件内容的 hash,别死磕插件的 ZSENS_VERSION——否则插件更新但子主题资源没动,缓存会捣乱。

有没有兄弟用其他思路解决过类似问题?比如直接在模板覆写时强制要求"资源同捆",或者干脆用 webpack 构建阶段把子主题覆写也打进去?想听听实战反馈。

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