本文约 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() 依次在以下位置找:
get_stylesheet_directory()(子主题目录)get_template_directory()(父主题目录)- 若都找不到,回退到层级里的下一个模板名
机制二: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 区分大小写)、目录层级是否一致、缓存是否已清。
常见踩坑
- 目录层级对不上:把
template-parts/content.php复制到了子主题根目录,等于新建了一个没人引用的文件。 - 只改了文件名后缀:
content-single.php与content-single-post.php是不同层级,别想当然。 - 页面模板没切换:复制后下拉框出现两个同名选项,页面仍指向父主题的那个。
- 忘记清缓存:对象缓存里存着旧的文件清单,新文件被忽略。
- 父主题用了
include而不是get_template_part:子主题无法通过同名文件覆盖,只能改用钩子(见子主题修改父主题钩子)。 - 案例:某站子主题里放了
woocommerce/archive-product.php,改了产品列表的列数,前台毫无变化。排查发现父主题自己也覆盖了这个模板(父目录里也有woocommerce/archive-product.php),而 WooCommerce 的查找顺序是子主题优先——问题其实出在父主题把最终的 HTML 结构又通过wc_get_template_part()拆到了content-product.php,真正需要覆盖的是那一份。教训是覆盖前先顺着模板里的get_template_part/wc_get_template调用链走一遍。 - 大小写不一致:本地 Windows/Mac 不区分大小写能跑,Linux 服务器上直接 404 或回退父主题。
- 子主题覆盖了
functions.php里require的文件:require不走模板查找机制,同名文件不会被自动替换,必须用get_theme_file_path()。
总结
记住三句话:主模板走模板层级,片段走 get_template_part,插件模板走插件自己的目录规则——三者都是子主题优先。覆盖时的动作顺序:先顺着调用链找到真正渲染目标 HTML 的那份文件 → 按原相对路径复制到子主题 → 清缓存 → 用 ?debug_tpl=1 确认加载路径。能用钩子改的尽量不覆盖(覆盖意味着父主题升级后你要自己对比差异),这一点与 子主题与父主题 functions.php 执行顺序 里讲的取舍是一致的;若只是想改样式,优先看 子主题加载父主题样式。
