useEntityRecords:一种更简便的获取 WordPress 数据的方法
有很多原因促使您希望从 WordPress 的 REST API 获取数据并在您的应用程序中使用它。
一个常见的例子是创建动态区块时。动态区块没有 save() 函数,而是使用服务器端渲染在前端呈现内容。如果您希望您的区块在编辑器中的外观与在前端相同,那么您的区块的 edit() 函数就需要从 REST API 获取内容。
这只是一个例子。您可能还能想到更多。
@wordpress/data 和 @wordpress/core-data 包提供了许多操作和选择器,使获取数据以及执行其他 CRUD 操作(CRUD 代表创建、读取、更新、删除)变得更加容易。这些操作和选择器的一些例子包括 getEntityRecords、editEntityRecord 和 deleteEntityRecord 等。
Learn WordPress 上有一个非常好的课程《使用 WordPress 数据层》,它引导您完成使用这些包执行 CRUD 操作的过程。该课程演示了您可能希望获取数据的另一个例子,即在管理页面上显示数据。
遵循该课程中使用的示例,本教程将向您展示如何获取页面列表。但不是在管理页面上显示列表,而是在一个区块中显示它。
在开始之前,您需要有一些页面来显示。最快最简单的方法是导入主题单元测试数据。本教程中的截图将显示区块展示由主题单元测试创建的页面。
创建插件
现在您的数据库中有了一些页面,可以开始构建一个区块来获取它们并在列表中显示页面标题。
@wordpress/create-block 包是快速搭建一个能创建此类区块的插件的好方法。为此,您将创建一个名为 list-pages 的动态区块。在本地测试站点的 wp-content/plugins 目录中运行以下命令:
npx @wordpress/create-block --variant dynamic list-pages
搭建过程需要一两分钟。完成后,激活插件。然后创建一篇新文章,并向文章添加一个“List Pages”区块。

保存文章。
您将在编辑器中看到:

在前端看到:

让我们先从前端视图开始,将其更改为显示页面列表,因为这相当简单。它只是一个获取最近 20 个页面的 WP_Query,然后是一个相当标准的文章循环实现,将页面标题显示在一个无序列表中,即作为 <ul> 中的 <li>,每个都链接到相关页面。如果没有页面,则显示一条消息说明情况。
将此代码添加到 src 目录中的 render.php 文件。(在本教程中,您将专门在 src 目录中工作,因此对于所有未来对文件的引用,您应假定它们在 src 目录中)
<?php
$args = array(
'post_type' => 'page',
'posts_per_page' => '20'
);
$pages = new WP_Query( $args );
if ( $pages->have_posts() ) {
$page_list = '<ul>';
while ( $pages->have_posts() ) {
$pages->the_post();
$page_list .= '<li><a href="'. get_the_permalink() . '">' . get_the_title() . '</a></li>';
}
$page_list .= '</ul>';
} else {
$page_list = 'No pages to display.';
}
wp_reset_postdata();
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<?php echo $page_list; ?>
</div>
保存文件并从 wp-content/plugins/list-pages 目录内运行 npm start。这将确保更改传播到 build 目录,并监视未来的更改并编译它们。
现在,如果您刷新前端,假设您已按照建议导入了主题单元测试数据,您将看到:

顺便说一下,如果您希望您的版本看起来相同,请在 style.scss 中设置 li a 的样式:
.wp-block-create-block-list-pages {
background-color: #d5e7ee;
padding: 2px;
li a {
color: rgb(32, 81, 167);
}
}
在编辑器中查看列表
目前编辑器仍然显示与之前相同的内容:

理想情况下,编辑器应该看起来像前端。让我们看看如何将相同的内容获取到编辑器中的区块。
让我们从使用 getEntityRecords 开始,就像前面提到的课程中所做的那样。然后您稍后会看到 useEntityRecords 如何使用更少的代码,并使检查查询是否已解析之类的事情变得容易得多。
首先,在 edit.js 中创建一个 <PagesList> 组件:
function PagesList( { pages } ) {
if ( !pages?.length )
return <div>No pages to display</div>;
return (
<ul>
{ pages?.map( page => (
<li key={ page.id }>
{ page.title.rendered }
</li>
) ) }
</ul>
);
}
该组件从传递给它的 props 对象中解构出 pages,并返回一个由从页面数组中获取的标题组成的无序列表。如果数组为空,即没有页面,它将返回字符串“No pages to display”。
当然,该组件需要传递一个页面数组,所以让我们从 WordPress 数据库中获取它。
在 edit.js 顶部添加这两个导入语句:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
接下来,用这个替换 Edit() 函数:
export default function Edit() {
const pages = useSelect(
select =>
select( coreDataStore ).getEntityRecords( 'postType', 'page', { per_page: 20 } ),
[]
);
return <PagesList pages={ pages }/>;
}
useSelect 是一个从 @wordpress/data 包导入的自定义 React Hook。它接收一个回调函数和一个依赖项数组(在本例中为空)。
回调函数使用存储的 getEntityRecords 函数来检索最近的二十个页面——这是在传递给 getEntityRecords 的第三个参数(查询对象)中定义的。另外两个参数是实体种类,这里是 postType,以及实体名称,在本例中是 page。
现在,如果您重新加载编辑器页面,您的区块应该看起来像这样:

但是有一个问题!
如果您仔细观察,您会注意到在数据仍在加载时,“No pages to display”文本会出现一小段时间。最好将其替换为加载指示器,以便只有在确实没有页面时才显示“No pages to display”。
添加加载指示器
为此,首先导入 <Spinner> 组件:
import { Spinner } from '@wordpress/components';
接下来,useSelect 回调函数需要返回一个对象。pages 不再是 const 的准确名称,因此将其更改为 data:
const data = useSelect(
select => {
return {
pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', { per_page: 20 } ),
hasResolved: select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', ['postType', 'page', { per_page: 20 } ] )
}
},
[]
);
之前 getEntityRecords 查询的结果现在存储在对象的 pages 属性中,并且该对象还有一个额外的属性 hasResolved,它包含由 hasFinishedResolution 返回的布尔值,当查询仍在解析时为 false。
将这两个都作为 props 传递给 <PagesList> 组件:
<PagesList hasResolved={ data.hasResolved } pages={ data.pages } />
然后,在 <PagesList> 组件中解构额外的 prop,检查其值,如果为 false 则显示 <Spinner> 组件:
function PagesList( { hasResolved, pages } ) {
if ( !hasResolved )
return <Spinner />;
...
}
您整个 edit.js 文件现在应该看起来像这样:
import { useBlockProps } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { Spinner } from '@wordpress/components';
export default function Edit() {
const data = useSelect(
select => {
return {
pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', { per_page: 20 } ),
hasResolved: select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', ['postType', 'page', { per_page: 20 } ] )
}
},
[]
);
return (
<div { ...useBlockProps() }>
<PagesList hasResolved={ data.hasResolved } pages={ data.pages } />
</div>
);
}
function PagesList( { hasResolved, pages } ) {
if ( !hasResolved )
return <Spinner />;
if ( !pages?.length )
return <div>No pages to display</div>;
return (
<ul>
{ pages?.map( page => (
<li key={ page.id }>
{ page.title.rendered }
</li>
) ) }
</ul>
);
}
这段代码相当复杂,需要单独的函数调用来获取数据和确定数据是否仍在获取中——两者都需要传递完全相同的查询参数。此外,需要仔细阅读才能正确理解它。
改用 useEntityRecords
现在让我们看看如何通过使用 useEntityRecords 来以更简洁、更优雅、更易于使用的方法实现相同的功能。
useEntityRecords 是一个自定义的 React Hook,它的单数表亲是 useEntityRecord。它们分别是 getEntityRecords 和 getEntityRecord 的包装器。这可以从 useEntityRecords 的源代码中清楚地看到。
React Hook 是在函数组件内部使用的 JavaScript 函数,它“钩入”React 状态和生命周期方法。因此,Hook 可以是有状态的,也可以管理副作用。React 提供了几个内置的 Hook,例如 useState 和 useEffect,但也可以在您的应用程序中创建自定义 Hook,这正是 Gutenberg 开发人员所做的,useEntityRecords 就是这样一个自定义 Hook 的例子。
顺便说一下,自定义 React Hook 不要与 WordPress hooks 或 @wordpress/hooks package 混淆。它们是完全不同的东西。
React Hooks 必须在组件的顶层调用,即它们不能在条件语句或循环中使用。还有一些使用 hooks 的其他规则需要牢记。
现在让我们深入了解 useEntityRecords hook,看看使用它如何减少我们需要编写的代码量,以执行类似于上面使用 getEntityRecords 的获取操作。您将继续使用现有的 edit.js 文件。
首先,从 core data 包导入 useEntityRecords:
import { useEntityRecords } from '@wordpress/core-data';
您不再需要导入 store,因为它已经被导入到 use-entity-records.ts 中,这是定义 useEntityRecords hook 的文件。而且您将不再使用 useSelect,因此可以删除这两个导入语句:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
现在是魔法时刻 🪄。之前使用的 useSelect 语句中有些难以理解的代码现在可以用一行使用 useEntityRecords hook 的代码替换:
const data = useEntityRecords( 'postType', 'page', { per_page: 20 } );
useEntityRecords