title: "构建页面列表" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - Data Basics - How To Guides - Repos


构建页面列表

在这一部分,我们将构建一个可筛选的 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 请求,分别过滤 AAbAboAbouAbout。这些请求可能以不同于启动的顺序完成。有可能 search=Asearch=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] );

    // ...
}

还有最后一个问题。很容易出现拼写错误,导致传递给 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] );

    // ...
}

瞧!就是这样。

整合所有部分

所有组件都已就位,太棒了!以下是我们应用的完整 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 页面搜索查询无结果

接下来做什么?