Zsens Admin 插件模板覆写与资源热替换:我如何让子主题的 `overrides/` 目录同时"劫持"模板和修正静态资源前缀
之前写过子主题用 `get_template_part` 无视插件模板目录的事,这次换个更刁钻的场景——用户确实成功覆写了我的模板,但覆写后的模板里引用的图片、字体全炸了,因为路径还是插件原版的相对写法。
问题根源:模板继承和资源发布路径是两条独立链路,大多数人只打通了前半截。
我的覆写机制长这样
插件里注册模板栈,优先级:子主题 > 父主题 > 插件回退:
public function locate_template( $template_names, $load = false ) {
$located = '';
foreach ( (array) $template_names as $template_name ) {
if ( ! $template_name ) continue;
// 子主题优先
if ( file_exists( get_stylesheet_directory() . '/zsens-overrides/' . $template_name ) ) {
$located = get_stylesheet_directory() . '/zsens-overrides/' . $template_name;
break;
}
// 父主题兜底
if ( file_exists( get_template_directory() . '/zsens-overrides/' . $template_name ) ) {
$located = get_template_directory() . '/zsens-overrides/' . $template_name;
break;
}
// 插件默认
if ( file_exists( ZSENS_PLUGIN_DIR . 'templates/' . $template_name ) ) {
$located = ZSENS_PLUGIN_DIR . 'templates/' . $template_name;
break;
}
}
// ...
}
这套代码跑了一年没问题,直到有用户在子主题覆写了 `dashboard-header.php`,里面写了:
<link rel="stylesheet" href="assets/css/header.css">
<img src="images/logo.svg">
浏览器请求变成 `https://example.com/zsens-overrides/assets/css/header.css`——404。
坑点解剖
模板文件里的相对路径,解析基准是**当前请求的 URL**,不是模板文件在磁盘的位置。子主题的 `zsens-overrides/` 在 URL 空间里有暴露,但 `assets/` 子目录用户未必同步复制过去。就算复制了,用户改的是模板,凭什么要管我的资源?
更隐蔽的是:有些资源用户**想用自己的**(换品牌色),有些又**想沿用插件的**(通用图标)。一刀切的 `plugin_dir_url` 或 `get_stylesheet_directory_uri` 都解决不了。
我的解法:模板上下文注入 + 资源别名表
加载模板前,先往上下文里塞一个"资源解析器":
public function load_template( $_template_file, $args = [] ) {
// 计算这个模板文件所属的"来源标识"
$source = $this->detect_source( $_template_file );
// 'plugin' | 'parent-theme' | 'child-theme'
// 构建资源解析闭包,注入到模板作用域
$args['zsens_asset'] = function( $relative_path, $fallback_to_plugin = true ) use ( $source, $_template_file ) {
return $this->resolve_asset( $relative_path, $source, $_template_file, $fallback_to_plugin );
};
// 兼容 extract() 的传统模板写法
extract( $args, EXTR_SKIP );
include $_template_file;
}
解析逻辑核心:
private function resolve_asset( $relative, $source, $template_file, $fallback ) {
$relative = ltrim( $relative, '/' );
// 1. 子主题覆写了模板 → 优先去子主题同名资源目录找
if ( $source === 'child-theme' ) {
$child_asset = get_stylesheet_directory() . '/zsens-assets/' . $relative;
if ( file_exists( $child_asset ) ) {
return get_stylesheet_directory_uri() . '/zsens-assets/' . $relative;
}
// 用户可选:子主题没提供时,是回退插件还是留空?
if ( ! $fallback ) return '';
}
// 2. 父主题同理
if ( $source === 'parent-theme' ) {
$parent_asset = get_template_directory() . '/zsens-assets/' . $relative;
if ( file_exists( $parent_asset ) ) {
return get_template_directory_uri() . '/zsens-assets/' . $relative;
}
if ( ! $fallback ) return '';
}
// 3. 回退到插件原始资源,但要处理一个细节:
// 模板可能被覆写,但资源没覆写,此时路径基准变了,不能写死 plugin_dir_url
return $this->plugin_asset_url( $relative, dirname( $template_file ) );
}
这里埋了个小设计:`zsens-overrides/` 放模板,`zsens-assets/` 放资源,物理隔离,语义清晰。用户想换模板就覆写模板,想换资源就丢同名文件到 assets,互不干扰。
模板里的写法变成
<!-- 旧写法,路径基准随缘 -->
<img src="images/logo.svg">
<!-- 新写法,由运行时决定实际 URL -->
<img src="<?php echo esc_url( $zsens_asset('images/logo.svg') ); ?>">
如果用户彻底不想用插件的任何资源,传第二个参数:
<?php echo esc_url( $zsens_asset('images/logo.svg', false) ); ?>
// 子主题找不到就直接空字符串,不会偷偷回退
一个没料到的边界 case
有用户在子主题覆写了模板,但 `zsens-assets/` 里只放了 `logo.svg`,没放 `header.css`。按我的逻辑,CSS 会回退到插件版本,但颜色变量是子主题 CSS 定义的——结果页面蓝一块紫一块。
最后加了"资源组"概念,模板头声明依赖:
<?php
// 模板顶部元数据注释
/**
* Zsens Template: dashboard-header
* Asset Bundle: header-base
* Fallback Bundle: header-base
*/
?>
加载时整组校验,子主题必须提供完整 bundle,否则整套回退,避免碎片化覆写导致的样式撕裂。
这套机制上线后,模板覆写相关的支持工单从月均 12 张降到 2 张。核心经验:模板继承不是文件系统的镜像复制,是 URL 空间与磁盘路径的双重映射,必须给模板作者一个明确的"资源锚点",而不是让他们猜相对路径怎么写。
你们处理模板覆写时,资源路径是怎么设计的?有没有更轻量的做法?

