模板继承链断裂后,我发现静态资源路径在子主题与插件之间"踢皮球"

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

上周给一个企业客户做插件,功能本身不复杂:后台配一套数据,前台用自定义模板展示。结果在模板继承和静态资源发布上被坑了两天,记录一下。

先说场景。插件目录结构大概这样:

my-plugin/
├── assets/
│   ├── css/
│   │   └── frontend.css
│   └── js/
│       └── chart.js
├── templates/
│   ├── base.php          ← 插件"伪布局"
│   ├── archive-item.php
│   └── single-item.php
└── my-plugin.php

我的设想是:插件提供基础模板,主题开发者可以覆写。WordPress 的 locate_template() 机制支持这个,我用 get_template_part() 的过滤钩子让主题优先、插件兜底。问题出在"兜底"那层。

坑一:plugin_dir_path() 在模板继承链里的指向漂移

我在 base.php 里放了个导航组件,需要引用 frontend.css。最开始的写法:

<link rel="stylesheet" href="<?php echo plugin_dir_url( __FILE__ ) . 'assets/css/frontend.css'; ?>">

这行代码写在 templates/base.php 里,__FILE__ 指向插件目录,看起来没问题。但当主题覆写了 single-item.php、却没覆写 base.php 时,主题目录下的 single-item.php 通过 include plugin_dir_path( __FILE__ ) . 'base.php' 加载插件的 base 布局——此时 __FILE__ 突然变成了主题文件的路径,plugin_dir_url() 一脚踩空,CSS 404。

调试时我打印了 __FILE____DIR__,在继承链里跳来跳去,跟看魔术表演似的。

我的解法:不在模板里直接用 __FILE__,而是在插件主文件里定义一个常量,模板里只引用常量:

// my-plugin.php
define( 'MY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'MY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );

// templates/base.php
<link rel="stylesheet" href="<?php echo MY_PLUGIN_URL . 'assets/css/frontend.css'; ?>">

这样无论模板被谁 include,锚点都是插件根目录。

坑二:wp_enqueue_style 的 "src" 参数在子主题场景下的路径塌缩

我后来改用标准方式注册资源:

wp_enqueue_style(
    'my-plugin-frontend',
    MY_PLUGIN_URL . 'assets/css/frontend.css',
    [],
    '1.0.0'
);

本地开发没问题,上传到客户服务器后,子主题启用时样式丢失。排查发现客户用了子主题(child theme),而他们的 CDN 插件会把所有 .css 路径做重写,只认主题目录和 WordPress 核心目录下的资源,插件目录的被过滤掉了。

这不是我插件的 bug,但用户不会管。我需要一种"把静态资源伪装成主题资源"的方案,但又不能真的复制文件到主题目录——升级时会被覆盖。

最后的妥协方案:在插件激活时,用 WP_Filesystem 在主题的 my-plugin-assets/ 目录下创建符号链接(Linux)或复制文件(Windows 主机)。卸载钩子清理。代码大概这样:

function my_plugin_publish_assets() {
    $theme_dir  = get_stylesheet_directory(); // 子主题优先
    $target_dir = $theme_dir . '/my-plugin-assets';
    $source_dir = MY_PLUGIN_PATH . 'assets';
    
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    WP_Filesystem();
    global $wp_filesystem;
    
    if ( ! $wp_filesystem->is_dir( $target_dir ) ) {
        $wp_filesystem->mkdir( $target_dir );
    }
    
    // 遍历复制/链接
    foreach ( $wp_filesystem->dirlist( $source_dir ) as $file => $info ) {
        $src  = $source_dir . '/' . $file;
        $dest = $target_dir . '/' . $file;
        
        if ( function_exists( 'symlink' ) && ! is_link( $dest ) ) {
            @symlink( $src, $dest );
        } elseif ( ! $wp_filesystem->exists( $dest ) ) {
            $wp_filesystem->copy( $src, $dest );
        }
    }
    
    // 注册时改用主题路径
    add_filter( 'my_plugin_asset_base_url', function() {
        return get_stylesheet_directory_uri() . '/my-plugin-assets/';
    } );
}

注册资源时优先用过滤后的 URL:

$base_url = apply_filters( 'my_plugin_asset_base_url', MY_PLUGIN_URL . 'assets/' );
wp_enqueue_style( 'my-plugin-frontend', $base_url . 'css/frontend.css', [], '1.0.0' );

坑三:模板继承的"幽灵层级"

我最初想模仿 Laravel Blade 的 @extends('layout'),在 WordPress 里用手动 include 实现。但 WordPress 模板体系没有真正的"布局继承",get_header() / get_footer() 是片段组合,不是布局嵌套。

我的折中:用 output buffering 做内容捕获,模拟继承:

// templates/base.php
<?php
function my_plugin_render_layout( $content_callback ) {
    get_header();
    ?>
    <div class="my-plugin-wrapper">
        <aside>...侧边栏...</aside>
        <main>
            <?php call_user_func( $content_callback ); ?>
        </main>
    </div>
    <?php
    get_footer();
}

// templates/single-item.php
my_plugin_render_layout( function() {
    while ( have_posts() ) : the_post();
        ?>
        <article>
            <h1><?php the_title(); ?></h1>
            ...自定义字段...
        </article>
        <?php
    endwhile;
} );

主题覆写时,可以只覆写 single-item.php 里的回调内容,也可以连 base.php 一起覆写。我用 locate_template() 做优先级查找:

function my_plugin_locate_template( $template_name ) {
    $theme_template = locate_template( 'my-plugin/' . $template_name );
    if ( $theme_template ) {
        return $theme_template;
    }
    return MY_PLUGIN_PATH . 'templates/' . $template_name;
}

这个方案有个隐患:如果主题覆写了 base.php 但没覆写 single-item.php,而主题的 base.php 里没定义 my_plugin_render_layout(),就会炸。我加了个函数存在检查:

if ( ! function_exists( 'my_plugin_render_layout' ) ) {
    require_once MY_PLUGIN_PATH . 'templates/base.php';
}

但这又绕回了坑一的问题——require_once 里的路径必须用常量,不能用 __FILE__

一个没解决的纠结

符号链接方案在部分共享主机上被禁用,复制文件又担心升级同步问题。目前我的做法是:插件设置页加个"重新发布静态资源"按钮,升级后手动点一下。有没有更好的自动化方案?比如用 upgrader_process_complete 钩子自动触发?担心批量升级时 IO 爆炸。

另外,如果主题切换了,符号链接会留在旧主题目录里变成垃圾。我试了 switch_theme 钩子做清理+重新发布,但子主题切父主题时触发时机有点迷,有时清理了还没重新发布,页面就 404 了。这块有没有稳健的做法?

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