Zsens Admin 插件模板继承翻车记:当子主题 `get_template_part` 把我插件的 `templates/` 目录当成空气,我是怎么让静态资源路径跟着"宿主环境"走的
上周帮一个客户做插件定制,他的主题用了深度子主题继承,我的插件前端页面直接白了一半。排查下来是两个老问题撞在一起:模板继承链断裂,以及静态资源发布路径在子主题/多站点环境下"认错了爹"。
先说模板继承。我的插件结构大概长这样:
zsens-admin/
├── assets/
│ ├── css/
│ └── js/
├── templates/
│ ├── dashboard.php
│ └── partials/
│ └── _stats-card.php
└── zsens-admin.php
一开始我图省事,模板加载直接硬编码:
// 别学,这是翻车现场
function zsa_render_dashboard() {
include ZSA_PLUGIN_DIR . 'templates/dashboard.php';
}
客户子主题里放了个同名的 `zsens-admin/dashboard.php`,WP 的模板层级根本不理会,我的插件还是读自己的文件。客户要改样式只能动插件目录,升级就覆盖。
正确的姿势是接入 WP 的模板定位机制,让子主题能"覆盖"插件模板:
function zsa_locate_template( $template_name, $load = false ) {
// 子主题 / 父主题优先
$located = locate_template( 'zsens-admin/' . $template_name );
if ( ! $located ) {
$located = ZSA_PLUGIN_DIR . 'templates/' . $template_name;
}
if ( $load && $located ) {
load_template( $located, false );
}
return $located;
}
关键点在 `locate_template` 这个函数。它会按 子主题 → 父主题 → 传入的默认路径 的顺序找文件。我把插件模板统一放到 `zsens-admin/` 子目录下,避免跟主题自身文件冲突。现在客户只要在子主题里建 `zsens-admin/dashboard.php`,就能无痛覆盖。
但模板继承通了之后,静态资源路径又炸了。
我的 JS 里有个动态加载图表的接口,需要知道 `assets/js/` 的完整 URL。一开始用插件常量:
// 多站点子目录模式下会漏掉 /blog/ 前缀
const ZSA_ASSETS_URL = '';
本地单站点没问题,扔到客户的子目录多站点(`example.com/subsite/`)上,请求 `example.com/assets/js/chart.js` 直接 404。`ZSA_PLUGIN_URL` 是用 `plugins_url()` 取的,但我在某些页面把它写死到了内联脚本里,没经过 WP 的 URL 解析。
更隐蔽的是,有些主题会把插件资源通过 CDN 或合并工具处理,直接读插件目录的 URL 可能绕过这些优化。
现在的方案是分两层处理:
PHP 侧:统一用 `plugins_url()` 并且延迟解析
function zsa_asset_url( $path = '' ) {
// 允许主题或缓存插件通过 filter 接管
return apply_filters(
'zsa_asset_url',
plugins_url( 'assets/' . ltrim( $path, '/' ), ZSA_PLUGIN_FILE ),
$path
);
}
加 `apply_filters` 是给缓存插件、CDN 插件留钩子,它们可以批量改写资源域名。
JS 侧:不硬编码,从 data-attribute 读
// 模板里输出一次
<div id="zsa-app"
data-asset-base="<?php echo esc_url( zsa_asset_url() ); ?>">
</div>
// JS 里动态拼路径
const base = document.getElementById('zsa-app').dataset.assetBase;
fetch(`${base}js/chart-data.json`);
这样即使主题做了资源合并、域名切换,JS 拿到的始终是当前环境下的有效 URL。
还有个踩坑点:plugins_url() 在 mu-plugins 和常规 plugins 目录下返回值结构不一样。Must-Use 插件的路径是 `/wp-content/mu-plugins/`,如果用户把我的插件丢进 `mu-plugins`,上面那段代码不用改也能工作,但如果你在字符串拼接时假设了 `/plugins/` 固定路径,就会炸。
最后贴一个我现在的模板加载完整封装,兼顾了子主题覆盖、安全校验和调试标记:
function zsa_get_template( $slug, $name = null, $args = [] ) {
$templates = [];
if ( $name ) {
$templates[] = "zsens-admin/{$slug}-{$name}.php";
}
$templates[] = "zsens-admin/{$slug}.php";
// 让主题有机会覆盖
$template = locate_template( $templates );
if ( ! $template ) {
$template = ZSA_PLUGIN_DIR . "templates/{$slug}.php";
if ( $name && file_exists( ZSA_PLUGIN_DIR . "templates/{$slug}-{$name}.php" ) ) {
$template = ZSA_PLUGIN_DIR . "templates/{$slug}-{$name}.php";
}
}
if ( ! file_exists( $template ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
trigger_error( "ZSA template missing: {$slug}" );
}
return;
}
// 隔离 $args,避免污染
load_template( $template, false, $args );
}
调用方式跟 WP 原生习惯保持一致:
zsa_get_template( 'dashboard', 'pro', ['user_id' => get_current_user_id()] );
子主题里放 `zsens-admin/dashboard-pro.php` 就能接管,不传 `$name` 则匹配 `dashboard.php`。
你们插件的模板继承是怎么设计的?有没有遇到过主题缓存插件把 `plugins_url` 返回的地址又改了一道的情况?我这边测了 WP Rocket 和 LiteSpeed Cache 表现还不太一样,想听听你们的兼容经验。

