原作者:Justin Tadlock。来源:Building a book review grid with a Query Loop block variation。原文发表于2022年12月20日,以WordPress 6.1新增扩展能力为背景;插件头要求WordPress≥6.1、PHP≥7.4,不能视为2026年部署建议。

WordPress 6.1 为扩展 Query Loop(查询循环)区块增加了新的方法。这使插件开发者可以用较少代码扩展核心已有的查询与展示能力,而不必为每一种内容列表重新开发一个完整区块。
Query Loop 是区块化建站的重要基础,负责展示文章、页面和自定义文章类型。在6.1之前,与PHP中的 WP_Query 相比,它能直接完成的查询较有限,处理自定义文章类型等数据时,开发者常常需要独立造区块。新的扩展入口让开发者可以在核心查询循环的基础上增加查询条件。
原文举出的应用包括:按价格元字段展示商品网格、按地点列出企业目录、为互助筹款建立排行榜,以及按评分展示书评。本教程选择最后一种场景,从头建立一个插件,最后得到能够显示书评卡片并按星级筛选的 Query Loop 变体。方法可以继续扩展到更复杂的项目。
版本说明:这是2022年、以WordPress 6.1为背景的教程。下面完整保留教学步骤及代码,并在前台钩子实现后指出静态审核发现的问题。不要把历史示例直接当作已经适配当前版本、多个查询循环和所有插件组合的生产方案。

一、准备开发环境与示例内容
需要具备基本JavaScript知识,熟悉区块开发,并准备Node/npm开发工具、一个WordPress开发站点及代码编辑器。环境搭建可参考 Block Editor Handbook 的开发环境指南。
假设客户偶尔写书评,希望在站点的多个位置(例如自定义页面)展示最新书评。其内容已经归入名为“Book Reviews”的分类。请在开发环境重建这一场景:新建该分类、记下分类ID,再准备至少三篇属于这个分类的文章,并为每篇设置特色图片。稍后JavaScript中的分类常量必须使用这个真实ID。

二、建立插件及构建流程
在 wp-content/plugins 下新建插件目录,例如 book-reviews-grid。名称本身不是关键,下面的目录关系则需要对应:
book-reviews-grid
/index.php
/package.json
/src
/index.js
index.php 是主PHP文件,也可以换成其他名称。本文全部PHP逻辑都放在这个文件中。
PHP插件头
先加入最基本的插件信息。这里的WordPress与PHP最低版本是原文声明:
<?php
/**
* Plugin Name: Book Reviews Grid
* Version: 1.0.0
* Requires at least: 6.1
* Requires PHP: 7.4
*/
// Additional code goes here…
构建脚本
在 package.json 中加入 start 脚本。可以另外填写包名、描述等字段,但教程只需要这一个脚本。
{
"scripts": {
"start": "wp-scripts start"
}
}
安装 @wordpress/scripts 开发依赖:
npm install @wordpress/scripts --save-dev
配置完成后启动构建监听:
npm run start
其他可用命令见 @wordpress/scripts文档。这些命令仅在文中展示,本次整理未执行。原文没有锁定依赖版本,实际项目应确定目标WordPress/Gutenberg版本后固定相容的依赖与锁文件,不能假定今天安装的最新版还支持原文的全部环境组合。
三、先建立一个只按分类查询的变体
如果不引入自定义查询参数,一个简单 Query Loop 变体只需要少量注册代码。首先在 src/index.js 顶部导入 registerBlockVariation:
import { registerBlockVariation } from '@wordpress/blocks';
准备两个值:变体的唯一名称,以及前面创建的书评分类ID。原文以 book-reviews 和分类ID 8 为例;数字8必须替换成自己的分类ID。
const VARIATION_NAME = 'book-reviews';
const REVIEW_CATEGORY_ID = 8; // Assign custom category ID.
注册基本信息
为 core/query 注册变体,设置名称、标题、图标、说明及激活判断:
registerBlockVariation( 'core/query', {
name: VARIATION_NAME,
title: 'Book Reviews',
icon: 'book',
description: 'Displays a list of book reviews.',
isActive: [ 'namespace' ],
// Other variation options...
} );
两个值尤其关键:name 必须与唯一变体名称一致;isActive 使用包含 namespace 的数组。下一步为区块添加同名命名空间属性,WordPress据此识别当前是否使用这一变体。实际插件还应给名称加自己的稳定前缀,减少与其他插件重名的可能。
为变体设置查询与布局属性
变体的 attributes 可以使用 Query Loop 支持的属性,其中额外需要设置 namespace,并让它与变体名一致。这个例子显示书评分类中的最新六篇文章,使用宽对齐和三列网格:
registerBlockVariation( 'core/query', {
// ...Previous variation options.
attributes: {
namespace: VARIATION_NAME,
query: {
postType: 'post',
perPage: 6,
offset: 0,
taxQuery: {
category: [ REVIEW_CATEGORY_ID ]
}
},
align: 'wide',
displayLayout: {
type: 'flex',
columns: 3
}
},
// Other variation options...
} );
postType 选普通文章,perPage 设为6,offset 为0,taxQuery.category 限定书评分类。align 和 displayLayout 则为初始展示提供默认值。可根据主题与内容修改这些选项。
限制编辑器显示的核心控件
默认情况下,Query Loop 的控件都可以显示。通过 allowedControls 数组,可决定保留哪些选项。原文只保留排序与作者控件:
registerBlockVariation( 'core/query', {
// ...Previous variation options.
allowedControls: [
'order',
'author'
],
// Other variation options...
} );
完整选项说明见 允许控件文档。减少无关控件是在简化编辑体验,并不是建立服务端权限或查询参数的安全限制。
设置内部区块
最后配置默认内部区块。这个示例的第一个顶层内部区块为 core/post-template,它的子区块是文章特色图片与文章标题:
registerBlockVariation( 'core/query', {
// ...Previous variation options.
innerBlocks: [
[
'core/post-template',
{},
[
[ 'core/post-featured-image' ],
[ 'core/post-title' ]
],
]
]
} );
这些核心子区块没有额外定制,也可以继续加入其他区块或设置默认属性。更多注册选项见 Block Variations 与 Extending the Query Loop block。
组合方式:上面几段反复出现的 registerBlockVariation 是逐步展示同一个注册对象的不同部分。实际文件中应把基本信息、attributes、allowedControls、innerBlocks 合并到一次注册,省略号注释代表前面已写的内容,不是让同一个变体重复注册多次。
用PHP加载构建后的JavaScript
构建过程生成两个文件:build/index.js 是需要加载的脚本;build/index.asset.php 返回依赖数组和脚本版本。把下面代码加入主PHP文件,它会先检查资产文件是否存在,再把脚本入队到区块编辑器:
add_action( 'enqueue_block_editor_assets', 'myplugin_assets' );
function myplugin_assets() {
// Get plugin directory and URL paths.
$path = untrailingslashit( __DIR__ );
$url = untrailingslashit( plugins_url( '', __FILE__ ) );
// Get auto-generated asset file.
$asset_file = "{$path}/build/index.asset.php";
// If the asset file exists, get its data and load the script.
if ( file_exists( $asset_file ) ) {
$asset = include $asset_file;
wp_enqueue_script(
'book-reviews-variation',
"{$url}/build/index.js",
$asset['dependencies'],
$asset['version'],
true
);
}
}
准备好插件并在开发站点启用后,应能在区块插入器中找到“Book Reviews”,或通过 /book reviews 插入这个变体。若只需要核心 Query Loop 原本支持的查询条件,教程到这里即可结束。

四、把文章评分元数据接入变体
下面在已有代码上增加评分选择。用户选择星级后,列表只显示拥有对应元字段值的书评。这个阶段的核心是WordPress提供的查询过滤钩子;理解它们之后,可以推广到其他元字段和自定义文章类型。
准备 rating 元字段
为书评分类中的一篇或多篇文章增加名为 rating 的文章元字段,值为1到5。最简单的做法,是使用文章编辑界面的“自定义字段”面板。
如果看不到该面板,按原文路径在编辑器的三点菜单中打开 Preferences → Panels,启用 Custom Fields。然后逐篇设置评分。正式项目通常会提供专门的星级输入控件,减少录入错误;该编辑表单不在原文范围内。

导入控制面板组件
在 src/index.js 顶部增加以下导入,分别用于注册过滤器、放置检查器控件及创建面板与下拉框:
// ...Previous imports.
import { addFilter } from '@wordpress/hooks';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';
再建立一个辅助函数,根据区块的 namespace 判断它是否属于书评变体。前面定义的常量在这里继续使用:
const isBookReviewsVariation = ( props ) => {
const {
attributes: { namespace }
} = props;
return namespace && namespace === VARIATION_NAME;
};
加入星级选择
接下来创建一个面板组件,放入 SelectControl。原文使用下拉框,也可以改成单选列表、按钮组或自己的React组件。最重要的是:选择结果必须写入 props.attributes.query.starRating,后面的查询过滤器会读取它。
const BookReviewControls = ( { props: {
attributes,
setAttributes
} } ) => {
const { query } = attributes;
return (
<PanelBody title="Book Review">
<SelectControl
label="Rating"
value={ query.starRating }
options={ [
{ value: '', label: '' },
{ value: 1, label: "1 Star" },
{ value: 2, label: "2 Stars" },
{ value: 3, label: "3 Stars" },
{ value: 4, label: "4 Stars" },
{ value: 5, label: "5 Stars" }
] }
onChange={ ( value ) => {
setAttributes( {
query: {
...query,
starRating: value
}
} );
} }
/>
</PanelBody>
);
};
这段代码在更新 query 时先用 ...query 保留其他查询设置,再写入新评分。空选项表示没有指定星级。虽然选项里使用数字,控件返回的数据仍需按实际类型处理;服务端也必须单独验证,不能只依赖下拉框限定的五个值。
把控件装入区块检查器
通过 editor.BlockEdit 过滤器包裹编辑组件。如果当前区块符合书评变体,返回原编辑组件以及额外的 InspectorControls;否则只返回原编辑组件:
export const withBookReviewControls = ( BlockEdit ) => ( props ) => {
return isBookReviewsVariation( props ) ? (
<>
<BlockEdit {...props} />
<InspectorControls>
<BookReviewControls props={props} />
</InspectorControls>
</>
) : (
<BlockEdit {...props} />
);
};
addFilter( 'editor.BlockEdit', 'core/query', withBookReviewControls );
此时,选中书评变体应该可以看到“Book Review”面板与“Rating”下拉框。但选择评分还不会改变查询结果,因为还没有将 starRating 接到真正的查询参数。下面分别处理编辑器和前台。

五、让编辑器与前台使用同一评分条件
编辑器:过滤REST文章查询
编辑器通过REST接口取得文章。原文使用 rest_{$post_type}_query 钩子,当前文章类型为 post,所以实际钩子名是 rest_post_query。
这个过滤器会遇到该类型的所有相关查询,因此在修改参数前先检查是否带有自定义 starRating 参数。回调的 $request 是 WP_REST_Request 实例,可以用 get_param() 读取参数。如果值存在,就把元字段名和值写入查询参数:
add_filter( 'rest_post_query', 'myplugin_rest_book_reviews', 10, 2 );
function myplugin_rest_book_reviews( $args, $request ) {
$rating = $request->get_param( 'starRating' );
if ( $rating ) {
$args['meta_key'] = 'rating';
$args['meta_value'] = absint( $rating );
}
return $args;
}
按原文演示,此时在编辑器中选择五星,列表就只显示 rating 等于5的文章。absint() 把输入转换为非负整数,但它不保证数值在1到5之间。实际实现应明确允许值、拒绝其他范围并定义空值语义;REST请求可以绕开编辑器自行构造,前端下拉框不是验证边界。

前台:把评分写入区块查询参数
编辑器生效还不代表前台生效。原文先在 pre_render_block 上检查解析后的区块属性,再在其中为 query_loop_block_query_vars 加入匿名过滤器。这样,内部函数通过闭包取得 $parsed_block,便能读取该变体的评分并写入查询。作者也指出这种方式比较绕,希望将来能有更简单的办法。
add_filter( 'pre_render_block', 'myplugin_pre_render_block', 10, 2 );
function myplugin_pre_render_block( $pre_render, $parsed_block ) {
// Determine if this is the custom block variation.
if ( 'book-reviews' === $parsed_block['attrs']['namespace'] ) {
add_filter(
'query_loop_block_query_vars',
function( $query, $block ) use ( $parsed_block ) {
// Add rating meta key/value pair if queried.
if ( $parsed_block['attrs']['query']['starRating'] ) {
$query['meta_key'] = 'rating';
$query['meta_value'] = absint( $parsed_block['attrs']['query']['starRating'] );
}
return $query;
},
10,
2
);
}
return $pre_render;
}
在原文的单一变体演示中,前台与编辑器应显示相同的文章。以这种方式连接自定义控件和查询过滤器,可以扩展到其他内容列表,而不必重新开发整个区块。原文指出,教程虽然篇幅不短,总代码仍不到200行,相比从头做完整区块减少了许多工作。
静态审核:旧前台实现需要重点修正
上面的PHP按原文保留,便于对照,但有几个具体问题不能忽略。
第一,$parsed_block['attrs']['namespace'] 与 ['query']['starRating'] 都是未经存在性检查就直接读取。普通区块、缺省属性或没有选择评分时,可能产生PHP警告。实际代码应使用 isset()、空合并以及类型检查,先确认目标区块和字段存在,再读具体值。
第二,原文在每次符合条件的 pre_render_block 回调中追加匿名过滤器,却没有移除它。闭包保存了先前区块的属性,后续其他查询循环也可能经过这个过滤器,从而被错误地加上此前的评分条件。页面上出现多个不同评分的书评网格、普通文章列表或嵌套查询时,风险更明显。
修正方向是把查询过滤器的作用域严格绑定到当前目标Query Loop,依据当前实际区块上下文判定条件,避免把一次区块渲染的属性留在全局回调里影响后续区块;若确需临时注册,也应使用可移除的回调引用并在正确的生命周期清理。具体接法应依据目标版本的核心区块上下文与钩子签名实现,并通过多区块场景回归验证。本稿没有编造一段未经版本验证的“通用补丁”来替代原文。
第三,REST和前台都要验证评分在1至5的允许集合内,不能仅调用 absint()。还要避免与其他插件的查询条件互相覆盖,检查元字段查询的数据库成本,并确认分类ID、分页和主题网格布局符合实际站点。函数与过滤器命名也应加入自己的插件前缀。
上述是整理者通过静态阅读发现的边界,不是运行测试结果。本次没有安装插件,没有执行npm、PHP或JSX,也没有访问用户WordPress站点。上线前应至少验证无评分、1至5星、非法输入、多个不同评分网格、普通Query Loop混排、分页及编辑器/前台一致性。
继续扩展与原文致谢
这个练习的主要价值,是把已有的核心展示能力、自定义编辑器控件与两个查询入口连起来。换掉内容类型、元字段或控件后,同样的思路可以服务于商品目录、企业名单及排行榜。应先定义查询需求与数据结构,再选择需要扩展的部分。
作者感谢 @bph、@mburridge 与 @webcommsat 提供技术和编辑反馈。本文完整译写源站实质正文,未将评论区纳入正文;版权、原始署名和链接保留。
版权与来源:版权归Justin Tadlock及相应权利人;WordPress Developer Blog源页未单独列出本文文字及代码的开放许可证,未据WordPress软件许可推断文章许可。原文版权归原作者及相应权利人所有。












暂无评论内容