本文约 5,847 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:改一次卡片,要打开五个文件
首页、分类页、标签页、搜索页、作者页,五个模板里各有一段二十行的文章卡片 HTML。设计稿说卡片上加个阅读时长,于是五个文件逐个改;改到第三个发现另外两处的类名还不一样,样式又崩了。更糟的是,半年后接手的人不知道有五份副本,只改了一处,问为什么前台显示不一致。
get_template_part() 就是 WordPress 官方给这个问题准备的标准答案,Twenty 系列主题从 2013 年起就在用它。
原理分析:一次调用,两级查找
函数签名(WordPress 5.5 起新增第三个参数):
get_template_part( string $slug, string $name = null, array $args = array() )
它内部做的事可以概括为三步:
① 拼候选文件名
$name 非空 → "{slug}-{name}.php",同时保留 "{slug}.php" 作为备选
② 按目录顺序查找
子主题目录 → 父主题目录(get_stylesheet_directory() 优先于 get_template_directory())
也支持主题根目录下的子目录,如 template-parts/content-card.php
③ 找到第一个存在的文件后 include,并把 $args 提取为变量
关键点有三个:
| 特性 | 说明 |
|---|---|
| 有回退机制 | content-single.php 不存在时自动退回 content.php,不会报错 |
| 支持子主题覆盖 | 子主题放同名文件即可覆盖父主题片段,无需任何钩子 |
| 变量作用域隔离 | 片段文件里可直接使用主循环函数(the_title() 等),因为它在同一请求上下文里被 include |
| 无返回值 | 函数返回 void,不能拿它当”取 HTML 字符串”用 |
与相近函数的区别:
| 函数 | 用途 | 是否走子主题查找 |
|---|---|---|
get_template_part() |
引入可复用片段,有回退 | 是 |
locate_template() |
只返回文件路径,不 include | 是 |
get_theme_file_path() |
返回子主题/父主题中文件路径,不 include | 是 |
include get_stylesheet_directory() . '/...' |
硬编码路径 | 否,写死子主题 |
保姆级实操
第 1 步:规划片段目录
建议按”复用性”而不是”页面”来分:
template-parts/
├── content/
│ ├── content.php 文章卡片默认版
│ ├── content-card.php 网格卡片版
│ ├── content-list.php 紧凑列表版
│ └── content-none.php 无结果提示
├── header/
│ ├── branding.php 站点标识
│ └── navigation.php 主导航
└── common/
├── breadcrumb.php 面包屑
└── pagination.php 分页
第 2 步:把卡片抽成片段
<?php
/** template-parts/content/content-card.php —— 网格卡片 */
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( 'card' ); ?>>
<?php if ( has_post_thumbnail() ) : ?>
<a class="card__thumb" href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'medium_large', array( 'loading' => 'lazy' ) ); ?>
</a>
<?php endif; ?>
<h3 class="card__title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="card__meta">
<time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>">
<?php echo esc_html( get_the_date() ); ?>
</time>
<span><?php echo esc_html( get_the_author() ); ?></span>
</div>
<div class="card__excerpt"><?php the_excerpt(); ?></div>
</article>
第 3 步:在各模板里调用
// 首页 / 归档页:卡片版
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content/content', 'card' );
endwhile;
// 搜索页:列表版
while ( have_posts() ) :
the_post();
get_template_part( 'template-parts/content/content', 'list' );
endwhile;
// 无结果时
if ( ! have_posts() ) {
get_template_part( 'template-parts/content/content', 'none' );
}
注意调用写法:slug 传 'template-parts/content/content',name 传 'card',中间用第二个参数分隔而不是拼在一起,这是最容易写错的地方。写成 get_template_part( 'template-parts/content/content-card' ) 也能工作,但会失去”找不到 card 就退回默认版”的回退能力。
第 4 步:用 $args 给片段传参(5.5+)
同一个卡片在首页显示摘要、在侧边栏只显示标题,靠 $args 区分,不必建两个文件:
get_template_part( 'template-parts/content/content', 'card', array(
'show_excerpt' => true,
'thumb_size' => 'medium_large',
'class' => 'card--grid',
) );
片段里用 wp_parse_args() 兜底,避免调用方漏传参数:
<?php
$args = wp_parse_args( $args ?? array(), array(
'show_excerpt' => false,
'thumb_size' => 'thumbnail',
'class' => 'card',
) );
?>
<article <?php post_class( esc_attr( $args['class'] ) ); ?>>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<?php if ( $args['show_excerpt'] ) : ?>
<div class="card__excerpt"><?php the_excerpt(); ?></div>
<?php endif; ?>
</article>
如果需要兼容 WordPress 5.5 以下版本,用老办法 set_query_var() 传参,片段里用 get_query_var() 取:
set_query_var( 'show_excerpt', true );
get_template_part( 'template-parts/content/content', 'card' );
第 5 步:用钩子给片段加统一逻辑
get_template_part 触发三个动作钩子,可以在不改动片段文件的前提下统一注入内容:
// 在所有 content 片段输出前插入广告位或统计标记
add_action( 'get_template_part_template-parts/content/content', function ( $slug, $name, $args ) {
if ( 'card' === $name && ! empty( $args['show_ad'] ) ) {
echo '<div class="card__ad">…</div>';
}
}, 10, 3 );
// 通用钩子:get_template_part_{$slug},还有 get_template_part 本身
add_action( 'get_template_part', function ( $slug, $name ) {
// 记录片段调用情况,用于性能排查
}, 10, 2 );
第 6 步:确认片段被正确覆盖
子主题里放 template-parts/content/content-card.php 即可覆盖父主题同名片段。片段在整个模板链条里的位置,可对照WordPress 模板层级图解理解。若不确定查找到底走了哪个文件,临时打印:
add_filter( 'get_template_part', function ( $slug, $name ) { return $slug; }, 10, 2 );
var_dump( locate_template( array( "{$slug}-{$name}.php", "{$slug}.php" ) ) );
常见踩坑
- 把整个路径写进第一个参数。
get_template_part( 'template-parts/content/content-card' )能跑但失去回退,且钩子名会变成一长串。 - 在片段里写了
while ( have_posts() )。 片段是被主循环内部调用的,它只负责渲染”当前这一篇”,循环必须留在调用方。 - 忘记
wp_reset_postdata()。 片段里如果起了自己的WP_Query,结束必须重置,否则后续模板读到的还是子查询的数据,见WP_Query 与主查询的正确用法。 $args在 5.5 以下不存在。 用$args ?? array()兜底,或者老老实实用set_query_var()。- 用
get_template_part当”获取 HTML 字符串”用。 它是 void 函数,直接输出。需要拿字符串时用输出缓冲:ob_start(); get_template_part(...); $html = ob_get_clean();,但能用render_block()或自建函数返回的场景更好。 - 案例: 某站把文章卡片抽成了片段,但同时在
index.php顶部写了global $post; setup_postdata($post);。片段内the_permalink()拿到的永远是第一篇文章,整页十个卡片链接指向同一篇。原因是片段依赖的是”当前循环上下文”,调用方破坏了循环状态。正确做法是把setup_postdata()相关的自定义查询逻辑也走标准WP_Query+wp_reset_postdata()。 - 片段文件名用了大写或中文。 Linux 下
Content-Card.php与content-card.php是两个文件,查找会失败并静默退回默认版,表现为”我的覆盖没生效”且无任何报错。
总结
判断要不要抽片段的标准只有一个:同一段 HTML 在两处以上出现,或者未来很可能被复用。抽取时遵守三条约定:slug 传目录路径、变体放第二个参数以保留回退能力、片段内部只渲染不控制循环。这样改一次卡片,五个页面同时生效;子主题覆盖也只需新建一个同名文件,不需要碰父主题的任何代码。
