WP DEVELOP

EP157. “前台渲染:WP_Query 单篇查询与 HTML 模板拆分”

首页 WordPress 开发课程 新 BLOCK 类型练习 · EP157
约 20 分钟· #EP157#新 BLOCK 类型练习
🔒 登录后可标记已读

根据 Block 存下来的 profId,用 WP_Query 查出对应的 Professor 文章、拼出真正的推荐卡片 HTML(左边照片、右边标题/摘要/所教科目/详情链接)。这一讲同时示范了一种代码组织习惯:render_callback 本身只做「判断有没有 profId、有的话调用一个专门生成 HTML 的函数」这种「鸟瞰视角」的调度工作,真正的大段 HTML 模板拆到单独的 inc/generateProfessorHTML.php 文件里,PHP 顶部用 require_once 引入——避免主插件文件被大段 HTML 模板拖得又臭又长。


涉及文件

  • wp-content/plugins/featured-professor/featured-professor.php (修改)
  • wp-content/plugins/featured-professor/inc/generateProfessorHTML.php (新建)

代码实现

featured-professor.phprender_callback 只做判断和调度,顶部 require_once 引入模板文件

<?php

if( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

require_once plugin_dir_path(__FILE__) . 'inc/generateProfessorHTML.php';

class FeaturedProfessor {
  function __construct() {
    add_action('init', [$this, 'onInit']);
  }

  function onInit() {
    wp_register_script('featuredProfessorScript', plugin_dir_url(__FILE__) . 'build/index.js', array('wp-blocks', 'wp-i18n', 'wp-editor'));
    wp_register_style('featuredProfessorStyle', plugin_dir_url(__FILE__) . 'build/index.css');

    register_block_type('ourplugin/featured-professor', array(
      'render_callback' => [$this, 'renderCallback'],
      'editor_script' => 'featuredProfessorScript',
      'editor_style' => 'featuredProfessorStyle'
    ));
  }

  function renderCallback($attributes) {
    if ($attributes['profId']) {
      wp_enqueue_style('featuredProfessorStyle');
      return generateProfessorHTML($attributes['profId']);
    } else {
      return NULL;
    }
  }

}

$featuredProfessor = new FeaturedProfessor();

inc/generateProfessorHTML.php(新建,独立文件专门放 HTML 模板)

<?php

function generateProfessorHTML($id) {
  $profPost = new WP_Query(array(
    'post_type' => 'professor',
    'p' => $id
  ));

  while($profPost->have_posts()) {
    $profPost->the_post();
    ob_start(); ?>
    <div class="professor-callout">
      <div class="professor-callout__photo" style="background-image: url(<?php the_post_thumbnail_url('professorPortrait') ?>)"></div>
      <div class="professor-callout__text">
        <h5><?php the_title(); ?></h5>
        <p><?php echo wp_trim_words(get_the_content(), 30); ?></p>

        <?php
        $relatedPrograms = get_field('related_programs');
        if ($relatedPrograms) { ?>
          <p>Name teaches:
            <?php foreach($relatedPrograms as $key => $program) {
              echo get_the_title($program);
              if ($key != array_key_last($relatedPrograms) && count($relatedPrograms) > 1) {
                echo ', ';
              }
            } ?>.
          </p>
        <?php }
        ?>

        <p><strong><a href="<?php the_permalink() ?>">Learn more about <?php the_title() ?> &raquo;</a></strong></p>

      </div>
    </div>
    <?php
    wp_reset_postdata();
    return ob_get_clean();
  }
}

关键改动点:

  • render_callback 只做「有没有值」的判断和调度if ($attributes['profId'])——只有用户真的选过某位 Professor,才会往下走;否则直接 return NULL,不输出任何东西(比如刚插入 Block、还没选值就保存了文章,这种情况前台什么都不显示,不会报错或者显示一个空壳)
  • 代码组织习惯:把大段 HTML 模板拆到独立文件——featured-professor.php 顶部(class 定义之前)用 require_once plugin_dir_path(__FILE__) . 'inc/generateProfessorHTML.php' 引入一个新建的 inc/ 文件夹下的文件,这样主插件文件只需要关心「调度」,不需要关心「HTML 长什么样」这些细节,方便以后回来维护
  • wp_enqueue_style('featuredProfessorStyle'):只有真的要输出这张卡片时才加载 CSS——这个资源之前只在 onInit() 里用 wp_register_style() 注册过(只是登记,还没真正加载),这里才是真正决定「加载」的时机,避免完全没用到这个 Block 的页面也白白加载一份不会用到的 CSS
  • WP_Query 查单篇文章new WP_Query(array('post_type' => 'professor', 'p' => $id))——'p' => $id 是 WordPress 查询参数里专门用来「按文章 ID 查询单篇」的写法;同时依然指定 'post_type' => 'professor' 双重把关,即使传进来的 ID 因为某些异常情况不是真正的 Professor 文章类型,这个查询也不会误查出别的类型的文章
  • while($profPost->have_posts()) { $profPost->the_post(); ... }:标准的 WordPress 自定义查询循环写法——have_posts() 判断还有没有下一篇,the_post() 把当前这篇设置成「全局当前文章」,这样后面才能直接用 the_title()/get_the_content()/the_permalink() 这些依赖全局文章上下文的标准模板函数
  • 为什么要用 ob_start()/ob_get_clean():这个函数要求整体 return 一段 HTML 字符串(因为这是 render_callback 要用的返回值),但直接在 PHP 文件里写大段 HTML 又比较自然、可读性更高——用输出缓冲(ob_start() 开始「录制」接下来所有输出,中途可以随意穿插 HTML 和 PHP,最后 ob_get_clean() 把这段时间「录制」到的所有输出内容当作字符串取出并清空缓冲区)就能两者兼得:写起来像普通模板,返回值又是一整个字符串
  • the_post_thumbnail_url('professorPortrait'):读取之前在课程早期 Section 就注册过的图片尺寸 professorPortrait,作为 CSS background-image 的背景图,而不是用 <img> 标签——好处是配合 CSS 能更方便地让图片按容器裁切铺满,不用额外处理长宽比
  • wp_trim_words(get_the_content(), 30):WordPress 内建函数,把传入的字符串截断到指定单词数(这里是 30 个词),常用来生成摘要
  • get_field('related_programs'):读取之前用 Advanced Custom Fields 给 Professor 文章类型设置的「关联科目」字段,返回的是一个数组(可能为空)
  • foreach($relatedPrograms as $key => $program) { echo get_the_title($program); if (...) echo ', '; }:遍历关联科目数组,每个都输出标题;用 $key != array_key_last($relatedPrograms)(不是数组最后一项)并且 count($relatedPrograms) > 1(数组里不止一项)两个条件同时满足才加上 , 分隔符——这样保证「只有一个科目」时不会多余地输出逗号,「最后一个科目」后面也不会有逗号
  • array_key_last($数组):PHP 内建函数,返回数组最后一个元素的键名,常用来判断「当前是不是循环到最后一项」
  • PHP 里进出 <?php ?> 标签之间遗留的空白也算输出内容——本讲踩过这个坑:把结尾的句号 . 放在退出 PHP 标签之后(而不是紧跟在 echo 逻辑后面)时,PHP 标签和句号之间如果有换行/空格,会被原样输出成多余的空白字符,中间隔出一个看起来「多了一个空格」的视觉效果——解决办法是把这些收尾的静态字符紧贴着 ?> 标签写,不要空行/缩进
  • <?php the_permalink() ?>:文章详情页永久链接,配合 <?php the_title() ?> 拼出「Learn more about 教授名 »」这样的详情页跳转链接
  • wp_reset_postdata():因为用 new WP_Query() 执行了一次跟当前页面主查询完全无关的「自定义查询」,用完之后要调用这个函数把全局 $post 对象等状态还原回 WordPress 主查询原本设置的样子——避免影响页面剩余部分(比如页面其他地方还依赖全局文章数据的逻辑)

[截图:前台页面选定 Professor 后渲染出的推荐卡片,含左侧照片、右侧标题/摘要/所教科目/Learn more 链接]


Hook / Function 速查

名称类型用途
require_oncePHP 内建语法引入外部 PHP 文件的内容,多次引入同一文件只会真正执行一次
new WP_Query(array(...))WP 内建类执行自定义文章查询,'p' => $id 用于按文章 ID 精确查询单篇
ob_start() / ob_get_clean()PHP 内建 function开始/结束输出缓冲,把中间产生的所有输出收集成字符串返回
the_post_thumbnail_url($size)WP 内建 function获取指定图片尺寸的特色图片 URL
wp_trim_words($text, $numWords)WP 内建 function把字符串截断到指定单词数,常用于生成摘要
get_field($fieldName)ACF 插件 function读取 Advanced Custom Fields 自定义字段的值
array_key_last($array)PHP 内建 function返回数组最后一个元素的键名
wp_reset_postdata()WP 内建 function自定义查询循环结束后,把全局文章对象还原成主查询的状态

常见坑

  • 直接在 render_callback 里写一大段 HTML 字符串——文件会越写越臃肿,难以维护,最好拆到独立的模板文件里,render_callback 只保留调度逻辑
  • ?> 退出 PHP 标签后紧跟的静态文本前面如果有多余的换行/空格——会被原样输出成页面上肉眼可见的多余空白,静态收尾内容要紧贴 ?> 写,不要留空行
  • new WP_Query() 执行自定义查询后忘记调用 wp_reset_postdata()——全局 $post 状态会一直停留在这次自定义查询的最后一篇文章上,影响页面后续依赖全局文章数据的其他逻辑
  • 判断「是否需要逗号分隔符」时只检查「不是最后一项」,没有同时检查「数组元素个数是否大于 1」——数组只有一个元素时会误判成「不是最后一项」(如果用 != 单独判断可能出现逻辑错误),需要两个条件一起判断才能覆盖「只有一项」的边界情况

延伸 / 后续讲座会用到

下一讲要把这份front-end 已经写好的 HTML 模板,直接搬到编辑器后台复用,实现「选完 Professor 立刻看到跟前台一样的实时预览」。


Sources

Udemy:

  • Become a WordPress Developer: Unlocking Power With Code — Section 26, EP157