模板继承时 `locate_template` 把插件视图"吞"了:我用 `template_include` 过滤器搭了条"优先级索道"

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

上周接了个需求,要在用户个人中心页插一块插件专属的数据面板。听起来简单,我顺手在插件里写了 templates/user-dashboard-panel.php,然后用 get_template_part('user-dashboard-panel') 调——结果主题目录下同名文件一出现,我的代码直接"蒸发",连报错都没有。

问题出在 locate_template 的查找顺序:子主题 > 父主题 > 插件(压根不进插件)。这不是 bug,是 WordPress 的设计哲学,但对我这种想"提供默认视图、允许主题覆盖"的插件开发者来说,得自己铺条路。

我的解法:不抢主题的饭碗,但保证插件视图有"兜底"

核心思路是拦截 template_include,但只处理插件自己的命名空间,不动主题的正常逻辑。贴段关键代码:

add_filter('template_include', function ($template) {
    // 只接管插件自定义的模板标识
    if (is_singular('my_plugin_item') && !file_exists($template)) {
        $plugin_template = MY_PLUGIN_DIR . 'templates/single-my-plugin-item.php';
        return file_exists($plugin_template) ? $plugin_template : $template;
    }
    return $template;
}, 99); // 优先级放最后,让主题先选

template_include 管的是整页模板,我的需求只是"往现有页面里塞一块"。更细粒度的是 get_template_part 本身——它有两个钩子:get_template_part_{$slug}(先执行,可短路)和 get_template_part(后执行,传路径数组)。

我最后用的组合拳:

// 1. 注册插件模板目录到 locate_template 的"搜索范围"
add_filter('my_plugin_template_paths', function ($paths) {
    $paths[] = MY_PLUGIN_DIR . 'templates/';
    return $paths;
});

// 2. 在 get_template_part 阶段注入
add_filter('get_template_part_my-plugin/panel', function ($slug, $name) {
    $custom = MY_PLUGIN_DIR . "templates/{$slug}-{$name}.php";
    if (file_exists($custom)) {
        load_template($custom, false);
        return true; // 短路,不再走主题查找
    }
    return null;
}, 10, 2);

静态资源路径的"相对性噩梦"

模板继承搞定后,css 路径又炸了。我在 templates/user-dashboard-panel.php 里写 ../assets/style.css,主题一覆盖这个模板,相对基准就变了——因为实际执行的文件路径变成了主题目录。

不能用相对路径,也不能硬编码 URL。我的习惯是模板里只暴露一个全局变量或函数:

// 插件初始化时注册
function my_plugin_asset_url($path = '') {
    return plugins_url('assets/' . ltrim($path, '/'), __FILE__);
}

// 模板里这样用
<link rel="stylesheet" href="<?php echo esc_url(my_plugin_asset_url('css/panel.css')); ?>">

但这里有个坑:plugins_url 的第二个参数必须是"调用该函数的文件的绝对路径"。如果我把 my_plugin_asset_url 定义在 includes/utils.php,而模板在 templates/ 调用,__FILE__ 指向的是 utils.php 所在目录,往上退一级才是插件根目录。我为此专门做了路径校准:

define('MY_PLUGIN_FILE', __FILE__); // 在入口文件(插件根级 PHP)定义
define('MY_PLUGIN_DIR', plugin_dir_path(MY_PLUGIN_FILE));
define('MY_PLUGIN_URL', plugin_dir_url(MY_PLUGIN_FILE));

function my_plugin_asset_url($path = '') {
    return MY_PLUGIN_URL . 'assets/' . ltrim($path, '/');
}

一个隐蔽的缓存陷阱

最阴的是对象缓存 + 模板路径的 combo。有次我把模板从 templates/v1/ 挪到 templates/,前台还是加载旧路径。排查半天,发现是 wp_cache_get('my_plugin_template_' . $slug) 的缓存键没加版本号。现在我的缓存键长这样:

$cache_key = 'my_plugin_tpl_' . md5($slug . $name . MY_PLUGIN_VERSION);

现在我的模板加载流程

1. 主题/子主题有没有覆盖?有 → 用主题版本
2. 没有?查插件缓存
3. 缓存 miss?按 templates/slug-name.phptemplates/slug.php 降级查找
4. 找到后写缓存,同时把 resolved path 存在 $GLOBALS['my_plugin_loaded_templates'] 里,方便调试时一眼看清加载链

这套跑下来,主题作者能正常覆盖,插件升级不丢兜底,我自己排查也不用猜"到底加载的是哪个文件"。

你们处理插件视图继承时,是倾向用 template_include 全页接管,还是 get_template_part 局部注入?有没有遇到过主题用了 locate_template 但传了 $load = false 导致你的过滤器被跳过的 case?

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