title: "构建页面列表" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - Data Basics - How To Guides - Repos
构建页面列表
在这一部分,我们将构建一个可筛选的 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 钩子的作用:
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 接收两个参数:一个回调函数和依赖项。简而言之,每当依赖项或底层数据存储发生变化时,它都会重新运行回调函数。你可以在数据模块文档中了解更多关于 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 函数将它们替换为它们所代表的符号,如 á。
刷新页面应显示类似以下的列表:

Step 3: Turn it into a table
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 钩子,可以在 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 (
<div>
<SearchControl
onChange={ setSearchTerm }
value={ searchTerm }
/>
<PagesList pages={ pages }/>
</div>
)
}
搞定!现在我们可以筛选结果了:

### 使用 core-data 替代直接调用 API
让我们稍作停顿,思考一下另一种可能采用的方法——直接操作 API——的缺点。想象一下我们直接发送 API 请求:
```js
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 数据通过在后台处理异步部分来提供帮助。useSelect 会记住最近的调用,并只返回我们期望的数据。
其次,每次按键都会触发一个 API 请求。如果你输入 About,删除它,然后重新输入,总共会发出 10 个请求,即使我们可以重用数据。
Gutenberg 数据通过缓存由 getEntityRecords() 触发的 API 请求的响应,并在后续调用中重用它们来提供帮助。当其他组件依赖相同的实体记录时,这一点尤其重要。
总而言之,core-data 内置的工具旨在解决典型问题,以便你可以专注于应用程序本身。
步骤 5:加载指示器
我们的搜索功能存在一个问题。我们不太确定它是否仍在搜索,或者只是没有显示结果:

像“正在加载…”或“无结果”这样的提示信息可以澄清这一点。让我们来实现它们!首先,PagesList 需要知道当前状态:
import { SearchControl, Spinner } from '@wordpress/components';
function PagesList( { hasResolved, pages } ) {
if ( !hasResolved ) {
return <Spinner/>
}
if ( !pages?.length ) {
return <div>No results</div>
}
// ...
}
function MyFirstApp() {
// ...
return (
<div>
// ...
<PagesList hasResolved={ hasResolved } pages={ pages }/>
</div>
)
}
请注意,我们没有构建自定义的加载指示器,而是利用了 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 (
<div>
<SearchControl onChange={ setSearchTerm } value={ searchTerm } />
<PagesList hasResolved={ hasResolved } pages={ pages } />
</div>
);
}
function PagesList( { hasResolved, pages } ) {
if ( ! hasResolved ) {
return <Spinner />;
}
if ( ! pages?.length ) {
return <div>No results</div>;
}
return (
<table className="wp-list-table widefat fixed striped table-view-list">
<thead>
<tr>
<td>Title</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={ page.id }>
<td>{ decodeEntities( page.title.rendered ) }</td>
</tr>
) ) }
</tbody>
</table>
);
}
const root = createRoot(
document.querySelector( '#my-first-gutenberg-app' )
);
window.addEventListener(
'load',
function () {
root.render(
<MyFirstApp />
);
},
false
);
现在只需刷新页面,即可体验全新的状态指示器:
