搜索全站
主题 15 分钟阅读

get_template_part 用法详解:把重复模板拆成片段

同一个文章卡片在首页、归档、搜索页出现三次,改一次要改三处。讲清 get_template_part 的查找规则、$args 传参、子主题覆盖机制,以及与 include、locate_template 的区别。

U
UFO_PF 2022-11-17 · 阅读 15 分钟
ge 主题
阅读提示
本文约 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" ) ) );

常见踩坑

  1. 把整个路径写进第一个参数。 get_template_part( 'template-parts/content/content-card' ) 能跑但失去回退,且钩子名会变成一长串。
  2. 在片段里写了 while ( have_posts() )。 片段是被主循环内部调用的,它只负责渲染”当前这一篇”,循环必须留在调用方。
  3. 忘记 wp_reset_postdata()。 片段里如果起了自己的 WP_Query,结束必须重置,否则后续模板读到的还是子查询的数据,见WP_Query 与主查询的正确用法。
  4. $args 在 5.5 以下不存在。 用 $args ?? array() 兜底,或者老老实实用 set_query_var()。
  5. 用 get_template_part 当”获取 HTML 字符串”用。 它是 void 函数,直接输出。需要拿字符串时用输出缓冲:ob_start(); get_template_part(...); $html = ob_get_clean();,但能用 render_block() 或自建函数返回的场景更好。
  6. 案例: 某站把文章卡片抽成了片段,但同时在 index.php 顶部写了 global $post; setup_postdata($post);。片段内 the_permalink() 拿到的永远是第一篇文章,整页十个卡片链接指向同一篇。原因是片段依赖的是”当前循环上下文”,调用方破坏了循环状态。正确做法是把 setup_postdata() 相关的自定义查询逻辑也走标准 WP_Query + wp_reset_postdata()。
  7. 片段文件名用了大写或中文。 Linux 下 Content-Card.php 与 content-card.php 是两个文件,查找会失败并静默退回默认版,表现为”我的覆盖没生效”且无任何报错。

总结

判断要不要抽片段的标准只有一个:同一段 HTML 在两处以上出现,或者未来很可能被复用。抽取时遵守三条约定:slug 传目录路径、变体放第二个参数以保留回退能力、片段内部只渲染不控制循环。这样改一次卡片,五个页面同时生效;子主题覆盖也只需新建一个同名文件,不需要碰父主题的任何代码。

U
UFO_PF作者

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

留下你的想法

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