448 lines
15 KiB
Markdown
448 lines
15 KiB
Markdown
|
|
### 整合所有模块
|
|||
|
|
|
|||
|
|
所有组件已就位,太棒了!以下是我们应用的完整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
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
现在只需刷新页面即可体验全新的状态指示器:
|
|||
|
|
|
|||
|
|

|
|||
|
|

|
|||
|
|
|
|||
|
|
## 后续步骤
|
|||
|
|
|
|||
|
|
* **上一部分:** [环境设置](/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:加载指示器
|
|||
|
|
|
|||
|
|
我们的搜索功能存在一个问题。我们无法完全确定它仍在搜索还是显示无结果:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
像“加载中...”或“无结果”这样的几条消息可以澄清状态。让我们来实现它们!首先,`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页面列表。本节完成后,应用将呈现如下效果:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
让我们逐步了解实现过程。
|
|||
|
|
|
|||
|
|
## 步骤一:构建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 REST API](https://developer.wordpress.org/rest-api/)获取真实的页面列表。
|
|||
|
|
|
|||
|
|
首先请确保存在可获取的页面数据。在WPAdmin中通过侧边栏菜单进入“页面”栏目,确认至少存在四到五个页面:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
若页面不足,请创建新页面(可参考上图所示标题),注意务必执行*发布*操作而非仅*保存*。
|
|||
|
|
|
|||
|
|
接下来我们使用[`@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实体(如`á`),需要使用[`decodeEntities`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-html-entities/)函数将其转换为对应符号(如`á`)。
|
|||
|
|
|
|||
|
|
刷新页面后将显示类似这样的列表:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 步骤三:转换为表格形式
|
|||
|
|
|
|||
|
|
```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>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 步骤四:添加搜索框
|
|||
|
|
|
|||
|
|
当前页面列表虽然简短,但随着内容增长,操作会愈发困难。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/)组件。其实际效果如下:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
搜索框初始为空,输入内容会存储在`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>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
大功告成!现在我们可以对结果进行筛选了:
|
|||
|
|
|
|||
|
|

|