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

448 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### 整合所有模块
所有组件已就位太棒了以下是我们应用的完整JavaScript代码
```js
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页面时显示的加载指示器](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/indicator.jpg)
![WordPress页面搜索未找到结果](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/no-results.jpg)
## 后续步骤
* **上一部分:** [环境设置](/docs/how-to-guides/data-basics/1-data-basics-setup.md)
* **下一部分:** [构建编辑表单](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)
* 可选在block-development-examples代码库中查看[完整应用](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)
### 使用核心数据替代直接调用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] );
// ...
}
```
在核心数据之外进行操作,我们需要解决两个问题。
首先乱序更新。搜索“About”会触发五个API请求分别过滤`A`、`Ab`、`Abo`、`Abou`和`About`。这些请求的完成顺序可能与启动顺序不同。有可能_search=A_在_search=About_之后才解析完成从而导致我们显示错误的数据。
Gutenberg数据通过在幕后处理异步部分来解决这个问题。`useSelect`会记住最近的调用,并仅返回我们预期的数据。
其次每次按键都会触发一个API请求。如果你输入`About`删除它然后重新输入即使我们可以重用数据也会总共发出10个请求。
Gutenberg数据通过缓存由`getEntityRecords()`触发的API请求的响应并在后续调用中重用它们来解决这个问题。当其他组件依赖相同的实体记录时这一点尤其重要。
总而言之,核心数据内置的工具旨在解决典型问题,以便你可以专注于应用程序本身。
## 步骤5加载指示器
我们的搜索功能存在一个问题。我们无法完全确定它仍在搜索还是显示无结果:
![未找到匹配搜索条件的WordPress页面](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/unclear-status.jpg)
像“加载中...”或“无结果”这样的几条消息可以澄清状态。让我们来实现它们!首先,`PagesList`需要了解当前状态:
```js
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](https://developer.wordpress.org/block-editor/reference-guides/components/spinner/)组件。
我们仍然需要知道页面选择器`hasResolved`与否。我们可以使用`hasFinishedResolution`选择器来查明:
`wp.data.select('core').hasFinishedResolution( 'getEntityRecords', [ 'postType', 'page', { search: 'home' } ] )`
它接受选择器的名称和_你传递给该选择器的完全相同参数_如果数据已加载则返回`true`,如果我们仍在等待则返回`false`。让我们将其添加到`useSelect`中:
```js
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`的参数不同。确保它们完全相同至关重要。我们可以通过将参数存储在变量中来消除这种风险:
```js
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页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg)
让我们逐步了解实现过程。
## 步骤一构建PagesList组件
首先构建一个基础React组件来展示页面列表
```js
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页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/simple-list.jpg)
## 步骤二:获取数据
预设的示例页面并不实用。我们需要从[WordPress REST API](https://developer.wordpress.org/rest-api/)获取真实的页面列表。
首先请确保存在可获取的页面数据。在WPAdmin中通过侧边栏菜单进入“页面”栏目确认至少存在四到五个页面
![WordPress后台页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/pages-list.jpg)
若页面不足,请创建新页面(可参考上图所示标题),注意务必执行*发布*操作而非仅*保存*。
接下来我们使用[`@wordpress/core-data`](https://github.com/WordPress/gutenberg/tree/trunk/packages/core-data)包来处理WordPress核心API该包基于[`@wordpress/data`](https://github.com/WordPress/gutenberg/tree/trunk/packages/data)包构建。
通过[`getEntityRecords`](/docs/reference-guides/data/data-core/#getentityrecords)选择器获取页面列表该选择器会自动发起API请求、缓存结果并返回记录列表
```js
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )
```
在浏览器开发者工具中运行此代码会返回`null`,因为首次运行选择器后,`getEntityRecords`解析器才会请求页面数据。稍等片刻再次运行即可获取完整页面列表。
*注意:直接运行此命令需确保浏览器当前显示区块编辑器界面(任意页面均可),否则`select( 'core' )`函数将不可用并报错。*
同理,`MyFirstApp`组件需要在数据就绪后重新运行选择器,这正是`useSelect`钩子的作用:
```js
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`接收两个参数:回调和依赖项。其作用是在依赖项或底层数据存储变更时重新执行回调。可在[数据模块文档](/packages/data/README.md#useselect)中深入了解[useSelect](/packages/data/README.md#useselect)。
完整代码如下:
```js
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`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-html-entities/)函数将其转换为对应符号(如`á`)。
刷新页面后将显示类似这样的列表:
![网站页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/fetch-the-data.jpg)
## 步骤三:转换为表格形式
```js
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>
);
}
```
![展示网站页面标题的表格](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/make-a-table.jpg)
## 步骤四:添加搜索框
当前页面列表虽然简短但随着内容增长操作会愈发困难。WordPress管理员通常通过搜索框解决这个问题——现在让我们也来实现一个
首先添加搜索字段:
```js
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](https://developer.wordpress.org/block-editor/reference-guides/components/search-control/)组件。其实际效果如下:
![可搜索的WordPress页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter-field.jpg)
搜索框初始为空,输入内容会存储在`searchTerm`状态值中。若您不熟悉[useState](https://react.dev/reference/react/useState)钩子函数,可查阅[React官方文档](https://react.dev/reference/react/useState)了解更多。
现在我们可以仅请求匹配`searchTerm`的页面数据。查阅[WordPress API文档](https://developer.wordpress.org/rest-api/reference/pages/)可知,[/wp/v2/pages](https://developer.wordpress.org/rest-api/reference/pages/)接口支持`search`查询参数用于_限定返回匹配字符串的结果_。具体使用方法如下
```js
wp.data.select( 'core' ).getEntityRecords( 'postType', 'page', { search: 'home' } )
```
在浏览器开发者工具中运行此代码段,将触发请求至`/wp/v2/pages?search=home`(而非基础的`/wp/v2/pages`)。
接下来在`useSelect`调用中实现对应逻辑:
```js
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`组件代码如下:
```js
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页面列表显示'关于我们'](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter.jpg)