在上一部分中,我们添加了创建新页面的功能,在本部分我们将为我们的应用添加一个 删除 功能。

这是我们即将构建的内容预览:

添加删除按钮

步骤 1:添加删除按钮

让我们首先创建 DeletePageButton 组件并更新我们的 PagesList 组件的用户界面:

import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';

const DeletePageButton = () => (
    <Button variant="primary">
        Delete
    </Button>
)

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>
                    <td style={{width: 190}}>Actions</td>
                </tr>
            </thead>
            <tbody>
                { pages?.map( ( page ) => (
                    <tr key={page.id}>
                        <td>{ decodeEntities( page.title.rendered ) }</td>
                        <td>
                            <div className="form-buttons">
                                <PageEditButton pageId={ page.id } />
                                {/* ↓ This is the only change in the PagesList component */}
                                <DeletePageButton pageId={ page.id }/>
                            </div>
                        </td>
                    </tr>
                ) ) }
            </tbody>
        </table>
    );
}

现在的 PagesList 应该看起来像这样:

添加删除按钮

步骤 2:将按钮连接到删除操作

在 Gutenberg 数据中,我们使用 deleteEntityRecord 操作从 WordPress REST API 删除实体记录。它会发送请求、处理结果并更新 Redux 状态中的缓存数据。

以下是如何在浏览器的开发工具中尝试删除实体记录:

// We need a valid page ID to call deleteEntityRecord, so let's get the first available one using getEntityRecords.
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;

// Now let's delete that page:
const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );

// promise gets resolved or rejected when the API request succeeds or fails.

一旦 REST API 请求完成,您会注意到列表中有一个页面消失了。这是因为该列表由 useSelect() hook 和 select( coreDataStore ).getEntityRecords( 'postType', 'page' ) selector 填充。每当底层数据发生变化时,列表就会使用新数据进行重新渲染。这非常方便!

当点击 DeletePageButton 时,让我们分发该操作:

const DeletePageButton = ({ pageId }) => {
    const { deleteEntityRecord } = useDispatch( coreDataStore );
    const handleDelete = () => deleteEntityRecord( 'postType', 'page', pageId );
    return (
        <Button variant="primary" onClick={ handleDelete }>
            Delete
        </Button>
    );
}

步骤 3:添加视觉反馈

点击 删除 按钮后,REST API 请求可能需要几秒钟才能完成。让我们使用与教程前几部分中相同的 <Spinner /> 组件来传达这一信息。

我们需要 isDeletingEntityRecord selector。它与我们在第 3 部分中已经见过的 isSavingEntityRecord selector 类似:它返回 true 或 false,并且永远不会发出任何 HTTP 请求:

const DeletePageButton = ({ pageId }) => {
    // ...
    const { isDeleting } = useSelect(
        select => ({
            isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
        }),
        [ pageId ]
    )
    return (
        <Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
            { isDeleting ? (
                <>
                    <Spinner />
                    Deleting...
                </>
            ) : 'Delete' }
        </Button>
    );
}

这是实际运行的效果:

添加删除按钮

步骤 4:处理错误

我们乐观地假设 删除 操作总会成功。不幸的是,在底层,这是一个 REST API 请求,可能会以多种方式失败:

  • 网站可能已宕机。
  • 删除请求可能无效。
  • 在此期间页面可能被其他人删除了。

为了在这些错误发生时告知用户,我们需要使用 getLastEntityDeleteError selector 提取错误信息:

// Replace 9 with an actual page ID
wp.data.select( 'core' ).getLastEntityDeleteError( 'postType', 'page', 9 )

以下是如何在 DeletePageButton 中应用它:

import { useEffect } from 'react';
const DeletePageButton = ({ pageId }) => {
    // ...
    const { error, /* ... */ } = useSelect(
        select => ( {
            error: select( coreDataStore ).getLastEntityDeleteError( 'postType', 'page', pageId ),
            // ...
        } ),
        [pageId]
    );
    useEffect( () => {
        if ( error ) {
            // Display the error
        }
    }, [error] )

    // ...
}

error 对象来自 @wordpress/api-fetch,包含有关错误的信息。它具有以下属性:

  • message – 人类可读的错误消息,例如 Invalid post ID。
  • code – 基于字符串的错误代码,例如 rest_post_invalid_id。要了解所有可能的错误代码,您需要参考/v2/pages 端点的源代码。
  • data(可选)– 错误详情,包含失败请求的 HTTP 响应代码的 code 属性。

有许多方法可以将该对象转换为错误消息,但在本教程中,我们将显示 error.message。

WordPress 有一个使用 Snackbar 组件显示状态信息的既定模式。以下是在部件编辑器中的外观:

添加删除按钮

让我们在我们的插件中使用相同类型的通知!这有两部分:

  1. 显示通知
  2. 分发通知

显示通知

我们的应用程序只知道如何显示页面,但不知道如何显示通知。让我们告诉它吧!

WordPress 方便地为我们提供了渲染通知所需的所有 React 组件。名为 Snackbar 的组件代表单个通知:

添加删除按钮

不过我们不会直接使用 Snackbar。我们将使用来自 @wordpress/notices 的 SnackbarNotices,它可以显示多个通知,并使用平滑动画自动隐藏它们几秒钟后。事实上,WordPress 在部件编辑器和其他 wp-admin 页面中也使用了相同的组件!

让我们创建自己的 Notifications 组件:

import { SnackbarNotices } from '@wordpress/notices';

function Notifications() {
    return <SnackbarNotices className="notifications__snackbar" />;
}

基本结构已经就位。SnackbarNotices 会自动从通知存储中读取,所以您只需要在应用程序中渲染一次:

function MyFirstApp() {
    // ...
    return (
        <div>
            {/* ... */}
            <Notifications />
        </div>
    );
}

本教程专注于管理页面,不会详细讨论上述代码片段。如果您对 @wordpress/notices 的详细信息感兴趣,手册页面是一个很好的起点。

现在我们已经准备好告知用户可能发生的任何错误。

分发通知

有了 SnackbarNotices 组件,我们准备分发一些通知!方法如下:

import { useEffect } from 'react';
import { store as noticesStore } from '@wordpress/notices';
function DeletePageButton( { pageId } ) {
    const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
    // useSelect returns a list of selectors if you pass the store handle
    // instead of a callback:
    const { getLastEntityDeleteError } = useSelect( coreDataStore )
    const handleDelete = async () => {
        const success = await deleteEntityRecord( 'postType', 'page', pageId);
        if ( success ) {
            // Tell the user the operation succeeded:
            createSuccessNotice( "The page was deleted!", {
                type: 'snackbar',
            } );
        } else {
            // We use the selector directly to get the fresh error *after* the deleteEntityRecord
            // have failed.
            const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
            const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.'
            // Tell the user how exactly the operation has failed:
            createErrorNotice( message, {
                type: 'snackbar',
            } );
        }
    }
    // ...
}

太好了!DeletePageButton 现在完全能够处理错误了。让我们看看这个错误消息的实际效果。我们将触发一个无效的删除操作并让它失败。一种方法是乘以 pageId 一个大数:

function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
    pageId = pageId * 1000;
    // ...
}

一旦你刷新页面并点击任何 Delete 按钮,你应该会看到以下错误消息:

添加删除按钮

太棒了!我们现在可以删除 pageId = pageId * 1000; 行。

让我们现在尝试实际删除一个页面。这是你在刷新浏览器并点击删除按钮后应该看到的内容:

添加删除按钮

就是这样!

将所有内容串联起来

所有部件都已到位,太好了!这是我们在本章中做出的所有更改:

import { useState, useEffect } from 'react';
import { useSelect, useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';

function MyFirstApp() {
    const [searchTerm, setSearchTerm] = useState( '' );
    const { pages, hasResolved } = useSelect(
        ( select ) => {
            const query = {};
            if ( searchTerm ) {
                query.search = searchTerm;
            }
            const selectorArgs = ['postType', 'page', query];
            const pages = select( coreDataStore ).getEntityRecords( ...selectorArgs );
            return {
                pages,
                hasResolved: select( coreDataStore ).hasFinishedResolution(
                    'getEntityRecords',
                    selectorArgs,
                ),
            };
        },
        [searchTerm],
    );

    return (
        
); } function Notifications() { return ; } function PagesList( { hasResolved, pages } ) { if ( !hasResolved ) { return ; } if ( !pages?.length ) { return
No results
; } return ( { pages?.map( ( page ) => ( ) ) }
Title Actions
{ page.title.rendered }
); } function DeletePageButton( { pageId } ) { const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); // useSelect returns a list of selectors if you pass the store handle // instead of a callback: const { getLastEntityDeleteError } = useSelect( coreDataStore ) const handleDelete = async () => { const success = await deleteEntityRecord( 'postType', 'page', pageId); if ( success ) { // Tell the user the operation succeeded: createSuccessNotice( "The page was deleted!", { type: 'snackbar', } ); } else { // We use the selector directly to get the error at this point in time. // Imagine we fetched the error like this: // const { lastError } = useSelect( function() { /* ... */ } ); // Then, lastError would be null inside of handleDelete. // Why? Because we'd refer to the version of it that was computed // before the handleDelete was even called. const lastError = getLastEntityDeleteError( 'postType', 'page', pageId ); const message = ( lastError?.message || 'There was an error.' ) + ' Please refresh the page and try again.' // Tell the user how exactly the operation have failed: createErrorNotice( message, { type: 'snackbar', } ); } } const { deleteEntityRecord } = useDispatch( coreDataStore ); const { isDeleting } = useSelect( select => ( { isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ), } ), [ pageId ] ); return ( ); }

下一步是什么?