在本部分中,我们将构建一个可筛选的 WordPress 页面列表。这就是本节结束时应用程序的外观:

让我们看看如何逐步实现。
步骤 1:构建 PagesList 组件
让我们首先构建一个最小化的 React 组件来显示页面列表:
function MyFirstApp() {
const pages = [{ id: 'mock', title: 'Sample page' }]
return <PagesList pages={ pages }/>;
}
function PagesList( { pages } ) {
return (
<ul>
{ pages?.map( page => (
<li key={ page.id }>
{ page.title }
</li>
) ) }
</ul>
);
}
请注意,此组件尚未获取任何数据,仅显示硬编码的页面列表。当你刷新页面时,你应该看到以下内容:

步骤 2:获取数据
硬编码的示例页面并不太有用。我们希望显示您实际的 WordPress 页面,因此让我们从 WordPress REST API 获取实际的页面列表。
在我们开始之前,让我们确认我们确实有一些要获取的页面。在 WPAdmin 中,使用侧边栏菜单导航到“页面”,并确保它显示至少四或五个页面:

如果没有,请继续创建几个页面——您可以使用上面截图中相同的标题。请务必 发布 而不仅仅是 保存 它们。
现在我们已经有了要处理的数据,让我们深入代码。我们将利用 @wordpress/core-data 包,该包提供了解析器、选择器和操作符来与 WordPress 核心 API 一起工作。@wordpress/core-data 建立在 @wordpress/data 包之上。
要获取页面列表,我们将使用 getEntityRecords 选择器。简而言之,它将发出正确的 API 请求,缓存结果,并返回我们需要的记录列表。以下是如何使用它:
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )
如果您在浏览器的开发工具中运行以下代码片段,您将看到它返回 null。为什么?页面仅在首先运行 选择器 后由 getEntityRecords 解析器请求。如果您稍等片刻并重新运行它,它将返回所有页面的列表。
注意:要直接运行此类命令,请确保您的浏览器正在显示块编辑器实例(任何页面均可)。否则 select( 'core' ) 函数将不可用,您将收到错误。
同样,MyFirstApp 组件需要在数据可用时重新运行选择器。这正是 useSelect hook 所做的:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
const pages = useSelect(
select =>
select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
[]
);
// ...
}
function PagesList({ pages }) {
// ...
<li key={page.id}>
{page.title.rendered}
</li>
// ...
}
请注意,我们在 index.js 中使用 import 语句。这使得插件能够使用 wp_enqueue_script 自动加载依赖项。任何对 coreDataStore 的引用都编译为我们浏览器开发工具中使用的相同 wp.data 引用。
useSelect 接受两个参数:回调和依赖项。简而言之,每当依赖项或底层数据存储更改时,它都会重新运行回调。您可以在 data 模块文档 中了解有关 useSelect 的更多信息。
将它们组合在一起,我们得到以下代码:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
function MyFirstApp() {
const pages = useSelect(
select =>
select( coreDataStore ).getEntityRecords( 'postType', 'page' ),
[]
);
return <PagesList pages={ pages }/>;
}
function PagesList( { pages } ) {
return (
<ul>
{ pages?.map( page => (
<li key={ page.id }>
{ decodeEntities( page.title.rendered ) }
</li>
) ) }
</ul>
)
}
请注意,帖子标题可能包含 HTML 实体,如 á,因此我们需要使用 decodeEntities 函数将它们替换为它们代表的符号,如 á。
刷新页面应显示类似于以下列表的内容:

步骤 3:将其转换为表格
function PagesList( { pages } ) {
return (
<table className="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody>
{ pages?.map( page => (
<tr key={ page.id }>
<td>{ decodeEntities( page.title.rendered ) }</td>
</tr>
) ) }
</tbody>
</table>
);
}

步骤 4:添加搜索框
目前的页面列表很短;然而,随着它的增长,处理起来会越来越困难。WordPress 管理员通常通过使用搜索框来解决这个问题——让我们也实现一个!
让我们首先添加一个搜索字段:
import { useState } from 'react';
import { SearchControl } from '@wordpress/components';
function MyFirstApp() {
const [searchTerm, setSearchTerm] = useState( '' );
// ...
return (
<div>
<SearchControl
onChange={ setSearchTerm }
value={ searchTerm }
/>
{/* ... */ }
</div>
)
}
请注意,我们没有使用 input 标签,而是利用了 SearchControl 组件。它看起来是这样的:

该字段最初为空,内容存储在 searchTerm 状态值中。如果您不熟悉 useState hook,您可以在 React 文档 中了解更多。
我们现在可以请求仅匹配 searchTerm 的页面。
在与 WordPress API 文档 核对后,我们看到 /wp/v2/pages 端点接受 search 查询参数,并使用它来 将结果限制为匹配字符串的那些。但我们如何使用它?我们可以将自定义查询参数作为第三个参数传递给 getEntityRecords,如下所示:
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page', { search: 'home' } )
在浏览器的开发工具中运行此代码片段将触发对 /wp/v2/pages?search=home 的请求,而不是仅 /wp/v2/pages。
让我们在 useSelect 调用中镜像如下:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
// ...
const { pages } = useSelect( select => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
return {
pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query )
}
}, [searchTerm] );
// ...
}
现在,当提供 searchTerm 时,它被用作 search 查询参数。请注意,searchTerm 也指定在 useSelect 依赖项列表中,以确保当 searchTerm 更改时重新运行 getEntityRecords。
最后,这是我们将所有内容连接在一起后 MyFirstApp 的外观:
import { useState } from 'react';
import { createRoot } from 'react-dom';
import { SearchControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
const [searchTerm, setSearchTerm] = useState( '' );
const pages = useSelect( select => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
return select( coreDataStore ).getEntityRecords( 'postType', 'page', query );
}, [searchTerm] );
return (
)
}
好了!我们现在可以过滤结果了:

使用 core-data 而不是直接调用 API
让我们暂停一下,考虑一下我们可以采取的另一种方法的缺点——直接使用 API。想象我们直接发送 API 请求:
import apiFetch from '@wordpress/api-fetch';
function MyFirstApp() {
// ...
const [pages, setPages] = useState( [] );
useEffect( () => {
const url = '/wp-json/wp/v2/pages?search=' + searchTerm;
apiFetch( { url } )
.then( setPages )
}, [searchTerm] );
// ...
}
在 core-data 之外工作,我们需要解决两个问题。
首先,乱序更新。搜索“About”将触发五个 API 请求来过滤 A、Ab、Abo、Abou 和 About。这些请求可能以与开始不同的顺序完成。search=A 可能在 search=About 之后解析,因此我们将显示错误的数据。
Gutenberg data 通过处理幕后异步部分来帮助解决此问题。useSelect 记住最近的调用并仅返回我们期望的数据。
其次,每次按键都会触发 API 请求。如果您键入 About、删除它并重新输入它,它将总共发出 10 个请求,即使我们可以重用数据。
Gutenberg data 通过缓存由 getEntityRecords() 触发的 API 请求的响应并在后续调用中重用它们来帮助解决此问题。当其他组件依赖于相同的实体记录时,这一点尤为重要。
总而言之,core-data 中的实用程序旨在解决典型问题,以便您可以专注于您的应用程序。
第 5 步:加载指示器
我们的搜索功能有一个问题。我们不能完全确定它是否仍在搜索或没有结果:

几条消息,如 正在加载… 或 无结果 可以澄清这一点。让我们实现它们!首先,PagesList 必须了解当前状态:
import { SearchControl, Spinner } from '@wordpress/components';
function PagesList( { hasResolved, pages } ) {
if ( !hasResolved ) {
return ;
}
if ( !pages?.length ) {
return 无结果;
}
// ...
}
function MyFirstApp() {
// ...
return (
// ...
)
}
请注意,我们没有构建自定义加载指示器,而是利用了 Spinner 组件。
我们仍然需要知道页面选择器是否 hasResolved。我们可以使用 hasFinishedResolution 选择器来找出:
wp.data.select('core').hasFinishedResolution( 'getEntityRecords', [ 'postType', 'page', { search: 'home' } ] )
它接受选择器的名称和 您传递给该选择器的完全相同的参数,如果数据已加载则返回 true,如果我们仍在等待则返回 false。让我们将其添加到 useSelect:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
// ...
const { pages, hasResolved } = useSelect( select => {
// ...
return {
pages: select( coreDataStore ).getEntityRecords( 'postType', 'page', query ),
hasResolved:
select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', ['postType', 'page', query] ),
}
}, [searchTerm] );
// ...
}
还有一个最后的问题。很容易拼写错误并导致向 getEntityRecords 和 hasFinishedResolution 传递不同的参数。它们必须完全相同。我们可以通过将参数存储在变量中来消除这种风险:
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
function MyFirstApp() {
// ...
const { pages, hasResolved } = useSelect( select => {
// ...
const selectorArgs = [ 'postType', 'page', query ];
return {
pages: select( coreDataStore ).getEntityRecords( ...selectorArgs ),
hasResolved:
select( coreDataStore ).hasFinishedResolution( 'getEntityRecords', selectorArgs ),
}
}, [searchTerm] );
// ...
}
就是这样!
将所有内容连接起来
所有部件都已到位,太好了!这是我们的应用程序的完整 JavaScript 代码:
import { useState } from 'react';
import { createRoot } from 'react-dom';
import { SearchControl, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import './style.css';
function MyFirstApp() {
const [ searchTerm, setSearchTerm ] = useState( '' );
const { pages, hasResolved } = useSelect(
( select ) => {
const query = {};
if ( searchTerm ) {
query.search = searchTerm;
}
const selectorArgs = [ 'postType', 'page', query ];
return {
pages: select( coreDataStore ).getEntityRecords(
...selectorArgs
),
hasResolved: select( coreDataStore ).hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
};
},
[ searchTerm ]
);
return (
);
}
function PagesList( { hasResolved, pages } ) {
if ( ! hasResolved ) {
return ;
}
if ( ! pages?.length ) {
return 无结果;
}
return (
标题
{ pages?.map( ( page ) => (
{ decodeEntities( page.title.rendered ) }
) ) }
);
}
const root = createRoot(
document.querySelector( '#my-first-gutenberg-app' )
);
window.addEventListener(
'load',
function () {
root.render(
);
},
false
);
剩下的只是刷新页面并享受全新的状态指示器:

