在本部分中,我们将构建一个可筛选的 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>
    );
}

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

显示 Sample 页面的 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 hook 所做的:

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 接受两个参数:回调和依赖项。简而言之,每当依赖项或底层数据存储更改时,它都会重新运行回调。您可以在 data 模块文档 中了解有关 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 函数将它们替换为它们代表的符号,如 á。

刷新页面应显示类似于以下列表的内容:

网站页面列表

步骤 3:将其转换为表格

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 组件。它看起来是这样的:

可搜索的 WordPress 页面列表

该字段最初为空,内容存储在 searchTerm 状态值中。如果您不熟悉 useState hook,您可以在 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 (
        
) }

好了!我们现在可以过滤结果了:

显示关于我们的已过滤 WordPress 页面列表

使用 core-data 而不是直接调用 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] );
    // ...
}

在 core-data 之外工作,我们需要解决两个问题。

首先,乱序更新。搜索“About”将触发五个 API 请求来过滤 A、Ab、Abo、Abou 和 About。这些请求可能以与开始不同的顺序完成。search=A 可能在 search=About 之后解析,因此我们将显示错误的数据。

Gutenberg data 通过处理幕后异步部分来帮助解决此问题。useSelect 记住最近的调用并仅返回我们期望的数据。

其次,每次按键都会触发 API 请求。如果您键入 About、删除它并重新输入它,它将总共发出 10 个请求,即使我们可以重用数据。

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

总而言之,core-data 中的实用程序旨在解决典型问题,以便您可以专注于您的应用程序。

第 5 步:加载指示器

我们的搜索功能有一个问题。我们不能完全确定它是否仍在搜索或没有结果:

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

几条消息,如 正在加载… 或 无结果 可以澄清这一点。让我们实现它们!首先,PagesList 必须了解当前状态:

import { SearchControl, Spinner } from '@wordpress/components';
function PagesList( { hasResolved, pages } ) {
    if ( !hasResolved ) {
        return ;
    }
    if ( !pages?.length ) {
        return 
无结果
; } // ... } function MyFirstApp() { // ... return (
// ...
) }

请注意,我们没有构建自定义加载指示器,而是利用了 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 (
        
); } function PagesList( { hasResolved, pages } ) { if ( ! hasResolved ) { return ; } if ( ! pages?.length ) { return
无结果
; } return ( { pages?.map( ( page ) => ( ) ) }
标题
{ decodeEntities( page.title.rendered ) }
); } const root = createRoot( document.querySelector( '#my-first-gutenberg-app' ) ); window.addEventListener( 'load', function () { root.render( ); }, false );

剩下的只是刷新页面并享受全新的状态指示器:

搜索 WordPress 页面时显示的加载指示器
在 WordPress 中未找到页面搜索查询的结果

下一步是什么?