EP157. “前台渲染:WP_Query 单篇查询与 HTML 模板拆分”
🔒 登录后可标记已读根据 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.php:render_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() ?> »</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 的页面也白白加载一份不会用到的 CSSWP_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,作为 CSSbackground-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_once | PHP 内建语法 | 引入外部 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