gutenbergdocs/how-to-guides/data-basics/2-building-a-list-of-pages.md
2025-10-22 01:33:45 +08:00

15 KiB
Raw Blame History

整合所有模块

所有组件已就位太棒了以下是我们应用的完整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>暂无结果</div>;
	}

	return (
		<table className="wp-list-table widefat fixed striped table-view-list">
			<thead>
				<tr>
					<td>标题</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页面搜索未找到结果

后续步骤

使用核心数据替代直接调用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] );
	// ...
}

在核心数据之外进行操作,我们需要解决两个问题。

首先乱序更新。搜索“About”会触发五个API请求分别过滤AAbAboAbouAbout。这些请求的完成顺序可能与启动顺序不同。有可能_search=A_在_search=About_之后才解析完成从而导致我们显示错误的数据。

Gutenberg数据通过在幕后处理异步部分来解决这个问题。useSelect会记住最近的调用,并仅返回我们预期的数据。

其次每次按键都会触发一个API请求。如果你输入About删除它然后重新输入即使我们可以重用数据也会总共发出10个请求。

Gutenberg数据通过缓存由getEntityRecords()触发的API请求的响应并在后续调用中重用它们来解决这个问题。当其他组件依赖相同的实体记录时这一点尤其重要。

总而言之,核心数据内置的工具旨在解决典型问题,以便你可以专注于应用程序本身。

步骤5加载指示器

我们的搜索功能存在一个问题。我们无法完全确定它仍在搜索还是显示无结果:

未找到匹配搜索条件的WordPress页面

像“加载中...”或“无结果”这样的几条消息可以澄清状态。让我们来实现它们!首先,PagesList需要了解当前状态:

import { SearchControl, Spinner } from '@wordpress/components';
function PagesList( { hasResolved, pages } ) {
	if ( !hasResolved ) {
		return <Spinner/>
	}
	if ( !pages?.length ) {
		return <div>无结果</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] );

	// ...
}

还有最后一个问题。很容易出现拼写错误,最终传递给getEntityRecordshasFinishedResolution的参数不同。确保它们完全相同至关重要。我们可以通过将参数存储在变量中来消除这种风险:

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] );

	// ...
}

瞧!大功告成。

构建页面列表

在这一部分我们将构建一个可筛选的WordPress页面列表。本节完成后应用将呈现如下效果

可搜索的WordPress页面列表

让我们逐步了解实现过程。

步骤一构建PagesList组件

首先构建一个基础React组件来展示页面列表

function MyFirstApp() {
	const pages = [{ id: 'mock', title: '示例页面' }]
	return <PagesList pages={ pages }/>;
}

function PagesList( { pages } ) {
	return (
		<ul>
			{ pages?.map( page => (
				<li key={ page.id }>
					{ page.title }
				</li>
			) ) }
		</ul>
	);
}

注意该组件尚未获取真实数据,仅展示预设的页面列表。刷新页面后你将看到:

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

步骤二:获取数据

预设的示例页面并不实用。我们需要从WordPress REST API获取真实的页面列表。

首先请确保存在可获取的页面数据。在WPAdmin中通过侧边栏菜单进入“页面”栏目确认至少存在四到五个页面

WordPress后台页面列表

若页面不足,请创建新页面(可参考上图所示标题),注意务必执行发布操作而非仅保存

接下来我们使用@wordpress/core-data包来处理WordPress核心API该包基于@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函数将其转换为对应符号(如á)。

刷新页面后将显示类似这样的列表:

网站页面列表

步骤三:转换为表格形式

function PagesList( { pages } ) {
	return (
		<table className="wp-list-table widefat fixed striped table-view-list">
			<thead>
				<tr>
					<th>标题</th>
				</tr>
			</thead>
			<tbody>
				{ pages?.map( page => (
					<tr key={ page.id }>
						<td>{ decodeEntities( page.title.rendered ) }</td>
					</tr>
				) ) }
			</tbody>
		</table>
	);
}

展示网站页面标题的表格

步骤四:添加搜索框

当前页面列表虽然简短但随着内容增长操作会愈发困难。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查询参数用于_限定返回匹配字符串的结果_。具体使用方法如下

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的依赖项数组,确保在搜索词变更时重新执行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页面列表显示'关于我们'