Zsens Admin 插件模板继承翻车记:当子主题 `get_template_part` 把我插件的 `templates/` 目录当成空气,我是怎么让静态资源路径跟着"宿主环境"走的

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

上周帮一个客户做插件定制,他的主题用了深度子主题继承,我的插件前端页面直接白了一半。排查下来是两个老问题撞在一起:模板继承链断裂,以及静态资源发布路径在子主题/多站点环境下"认错了爹"。

先说模板继承。我的插件结构大概长这样:

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 表现还不太一样,想听听你们的兼容经验。

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