搜索全站
主题 12 分钟阅读

子主题覆盖父主题模板文件规则:同名覆盖与 get_template_part 差异

把 single.php 拷到子主题就能改,那 template-parts 呢?WooCommerce 模板呢?本文讲清 WordPress 的模板查找顺序、get_template_part 与 get_template_directory 的差异,以及覆盖了却不生效的排查方法。

U
UFO_PF 2023-07-27 · 阅读 12 分钟
子主 主题
阅读提示
本文约 4,761 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:同样叫「复制文件」,结果却不一样

子主题使用中最困惑的一件事:同样是「把父主题的文件复制到子主题再改」,有时立刻生效,有时完全没反应。复制 header.php 生效了;复制 template-parts/content.php 也生效了;但复制 page-templates/full-width.php 之后,后台页面模板下拉框里还是只有父主题的选项;复制 WooCommerce 的 single-product.php 又要在前面加一层 woocommerce/ 目录。

差别来自 WordPress 的几套查找机制:模板层级(Template Hierarchy)、get_template_part() / locate_template() 的目录搜索顺序、以及插件自带的模板路径规则。它们都遵循「子主题优先」,但搜索的目录与文件名规则各不相同。搞清楚这三条线,就不用再靠试错。

原理分析:三个查找机制

机制一:模板层级。 访问一个页面时,WordPress 按层级挑出模板名(如 single-post-{slug}.php → single-post.php → single.php → singular.php → index.php),然后调用 get_query_template(),其内部用 locate_template() 依次在以下位置找:

  1. get_stylesheet_directory()(子主题目录)
  2. get_template_directory()(父主题目录)
  3. 若都找不到,回退到层级里的下一个模板名

机制二:get_template_part()。 它按 {$slug}-{$name}.php → {$slug}.php 的顺序拼接文件名,同样先查子主题后查父主题。注意它只查两个主题目录,不会去插件目录。

机制三:插件模板(以 WooCommerce 为例)。 由插件自己的函数(如 wc_get_template())查找,顺序是:子主题/插件目录 → 父主题/插件目录 → 插件自带 templates 目录。所以路径前面要多一层 woocommerce/。

还有一个 WP 4.7 引入的便捷函数对:

get_theme_file_path( 'template-parts/content.php' );      // 子主题优先,找不到回父主题
get_parent_theme_file_path( 'template-parts/content.php' ); // 强制父主题

写主题时优先用 get_theme_file_path()(或 get_theme_file_uri()),它天生支持子主题覆盖——很多老主题的「子主题改不动」问题,根源就是父主题用了 get_template_directory() 这种写死的路径。

保姆级实操

第一步:覆盖标准模板文件

# 子主题目录:wp-content/themes/astra-child
cp wp-content/themes/astra/single.php        wp-content/themes/astra-child/single.php
cp wp-content/themes/astra/archive.php       wp-content/themes/astra-child/archive.php
cp wp-content/themes/astra/footer.php        wp-content/themes/astra-child/footer.php

规则很简单:相对路径与文件名完全保持一致。父主题在 template-parts/content-single.php,子主题就放在 template-parts/content-single.php,不能图省事放到根目录。

第二步:理解 get_template_part 的查找细节

父主题里这样调用:

get_template_part( 'template-parts/content', 'single' );

它会依次尝试(每个都在子主题先找一遍):

wp-content/themes/astra-child/template-parts/content-single.php
wp-content/themes/astra/template-parts/content-single.php
wp-content/themes/astra-child/template-parts/content.php
wp-content/themes/astra/template-parts/content.php

所以你既可以只覆盖 content-single.php(精确),也可以覆盖 content.php(影响所有引用它的地方)。

第三步:页面模板(Page Template)的特殊规则

自定义页面模板靠文件头注释识别:

<?php
/**
 * Template Name: 全宽页面
 * Template Post Type: page
 */

get_header();
?>
<main id="primary" class="site-main full-width">
    <?php
    while ( have_posts() ) :
        the_post();
        the_content();
    endwhile;
    ?>
</main>
<?php
get_footer();

这里有个关键差异:页面模板的缓存。WordPress 会把已注册的页面模板存进缓存(对象缓存或 transient),新复制的文件可能不出现在下拉框里。解决办法:

wp cache flush
wp transient delete --all

删掉旧缓存后刷新后台页面编辑页即可看到新模板。另外,子主题里不能只覆盖父主题的页面模板——因为它们是按 Template Name 注册的两个独立选项,下拉框里会同时出现父主题和子主题的「全宽页面」,你得手动在页面属性里切换过去。

第四步:覆盖 WooCommerce 模板

mkdir -p wp-content/themes/astra-child/woocommerce/single-product
cp wp-content/plugins/woocommerce/templates/single-product/title.php \
   wp-content/themes/astra-child/woocommerce/single-product/title.php

路径规则是 子主题/woocommerce/{原模板在 templates 下的相对路径}。后台「WooCommerce → 状态 → 工具」里有「模板」一栏会列出被覆盖的文件及版本是否过期。

第五步:让父主题支持被覆盖(父主题作者视角)

如果你在写父主题,请用这组函数而不是写死路径:

// 好:支持子主题覆盖
require get_theme_file_path( 'inc/setup.php' );
wp_enqueue_style( 'mf-style', get_theme_file_uri( 'assets/css/app.css' ) );

// 差:子主题无法通过同名文件覆盖
require get_template_directory() . '/inc/setup.php';

模板里引用片段也统一用 get_template_part() 而不是 include:

// 差
include get_template_directory() . '/template-parts/content.php';

// 好
get_template_part( 'template-parts/content' );

第六步:排查「覆盖了但不生效」

在子主题里临时加一段调试输出,确认加载的到底是哪份文件:

add_action( 'wp_footer', function() {
    if ( ! current_user_can( 'manage_options' ) || ! isset( $_GET['debug_tpl'] ) ) {
        return;
    }

    global $template;
    echo '<!-- 当前主模板:' . esc_html( str_replace( ABSPATH, '', $template ) ) . ' -->';
} );

访问任意页面加 ?debug_tpl=1,看源码注释里显示的是子主题路径还是父主题路径。同时确认:文件名大小写是否与父主题一致(Linux 区分大小写)、目录层级是否一致、缓存是否已清。

常见踩坑

  1. 目录层级对不上:把 template-parts/content.php 复制到了子主题根目录,等于新建了一个没人引用的文件。
  2. 只改了文件名后缀:content-single.php 与 content-single-post.php 是不同层级,别想当然。
  3. 页面模板没切换:复制后下拉框出现两个同名选项,页面仍指向父主题的那个。
  4. 忘记清缓存:对象缓存里存着旧的文件清单,新文件被忽略。
  5. 父主题用了 include 而不是 get_template_part:子主题无法通过同名文件覆盖,只能改用钩子(见 子主题修改父主题钩子)。
  6. 案例:某站子主题里放了 woocommerce/archive-product.php,改了产品列表的列数,前台毫无变化。排查发现父主题自己也覆盖了这个模板(父目录里也有 woocommerce/archive-product.php),而 WooCommerce 的查找顺序是子主题优先——问题其实出在父主题把最终的 HTML 结构又通过 wc_get_template_part() 拆到了 content-product.php,真正需要覆盖的是那一份。教训是覆盖前先顺着模板里的 get_template_part / wc_get_template 调用链走一遍。
  7. 大小写不一致:本地 Windows/Mac 不区分大小写能跑,Linux 服务器上直接 404 或回退父主题。
  8. 子主题覆盖了 functions.php 里 require 的文件:require 不走模板查找机制,同名文件不会被自动替换,必须用 get_theme_file_path()。

总结

记住三句话:主模板走模板层级,片段走 get_template_part,插件模板走插件自己的目录规则——三者都是子主题优先。覆盖时的动作顺序:先顺着调用链找到真正渲染目标 HTML 的那份文件 → 按原相对路径复制到子主题 → 清缓存 → 用 ?debug_tpl=1 确认加载路径。能用钩子改的尽量不覆盖(覆盖意味着父主题升级后你要自己对比差异),这一点与 子主题与父主题 functions.php 执行顺序 里讲的取舍是一致的;若只是想改样式,优先看 子主题加载父主题样式。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。