跳转到主要内容

构建页面列表

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

可搜索的 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>
	);
}

请注意,此组件尚未获取任何数据,仅呈现硬编码的页面列表。刷新页面时,您应该看到以下内容:

显示示例页面的 WordPress 页面列表

步骤 2:获取数据

硬编码的示例页面没什么用。我们想要显示您实际的 WordPress 页面,所以让我们从 WordPress REST API 获取实际的页面列表。

开始之前,先确认我们确实有一些页面可以获取。在 WPAdmin 中,使用侧边栏菜单导航到“页面”,确保它至少显示四到五个页面:

WordPress 管理后台页面列表

如果没有,请创建几个页面——您可以使用上图中相同的标题。请确保发布它们,而不仅仅是保存。

现在我们有了可用的数据,让我们深入代码。我们将利用 @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 实体,如 &aacute;,因此我们需要使用 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>
	);
}

Table listing website page titles

步骤 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 组件。它的外观如下:

可搜索的 WordPress 页面列表

该字段初始为空,其内容存储在 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>
)

}


搞定!现在我们可以筛选结果了:

![筛选后的 WordPress 页面列表显示 About us](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter.jpg)

### 使用 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:加载指示器

我们的搜索功能存在一个问题。我们不太确定它是否仍在搜索,或者只是没有显示结果:

未找到与搜索查询匹配的 WordPress 页面

像“正在加载…”或“无结果”这样的提示信息可以澄清这一点。让我们来实现它们!首先,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
);

现在只需刷新页面,即可体验全新的状态指示器:

搜索 WordPress 页面时显示的加载指示器 WordPress 页面搜索查询无结果

接下来做什么?