本部分介绍如何为我们的应用添加一个 编辑 功能。这是我们即将构建的内容预览:

步骤 1:添加编辑按钮
如果没有 编辑 按钮,我们就无法拥有 编辑 表单,因此让我们首先在我们的 PagesList 组件中添加一个:
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
const PageEditButton = () => (
<Button variant="primary">
Edit
</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: 120}}>Actions</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={page.id}>
<td>{ decodeEntities( page.title.rendered ) }</td>
<td>
<PageEditButton pageId={ page.id } />
</td>
</tr>
) ) }
</tbody>
</table>
);
}
PagesList 中唯一的更改是添加了标记为 操作 的额外列:

步骤 2:显示编辑表单
我们的按钮看起来不错,但目前还没有任何功能。要显示编辑表单,我们需要先创建一个 – 让我们创建它:
import { Button, TextControl } from '@wordpress/components';
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
return (
<div className="my-gutenberg-form">
<TextControl
value=''
label='Page title:'
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
Save
</Button>
<Button onClick={ onCancel } variant="tertiary">
Cancel
</Button>
</div>
</div>
);
}
现在让我们使按钮显示我们刚刚创建的表单。由于本教程不专注于网页设计,我们将使用需要最少代码的组件将两者连接起来:Modal。让我们相应地更新 PageEditButton:
import { Button, Modal, TextControl } from '@wordpress/components';
function PageEditButton({ pageId }) {
const [ isOpen, setOpen ] = useState( false );
const openModal = () => setOpen( true );
const closeModal = () => setOpen( false );
return (
<>
<Button
onClick={ openModal }
variant="primary"
>
Edit
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="Edit page">
<EditPageForm
pageId={pageId}
onCancel={closeModal}
onSaveFinished={closeModal}
/>
</Modal>
) }
</>
)
}
现在点击 编辑 按钮,您应该看到以下模态框:

太好了!我们现在有了一个基本的工作用户界面。
步骤 3:用页面详情填充表单
我们希望 EditPageForm 显示当前编辑页面的标题。您可能注意到它没有接收 page prop,只接收 pageId。没关系。Gutenberg Data 允许我们轻松从任何组件访问实体记录。
在这种情况下,我们需要使用 getEntityRecord 选择器。记录列表已经通过 MyFirstApp 中的 getEntityRecords 调用可用,因此甚至不会涉及任何额外的 HTTP 请求 – 我们会立即获取缓存的记录。
这是如何在浏览器的开发工具中尝试它的:
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ); // Replace 9 with an actual page ID
让我们相应地更新 EditPageForm:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const page = useSelect(
select => select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId ),
[pageId]
);
return (
<div className="my-gutenberg-form">
<TextControl
label='Page title:'
value={ page.title.rendered }
/>
{ /* ... */ }
</div>
);
}
现在它应该看起来像这样:

步骤 4:使页面标题字段可编辑
我们的 页面标题 字段有一个问题:您无法编辑它。它接收一个固定的 value,但在输入时不会更新它。我们需要一个 onChange 处理程序。
您可能在其他 React 应用中见过类似的模式。这被称为 “受控组件”:
function VanillaReactForm({ initialTitle }) {
const [title, setTitle] = useState( initialTitle );
return (
<TextControl
value={ title }
onChange={ setTitle }
/>
);
}
在 Gutenberg Data 中更新实体记录是类似的,但不同于使用 setTitle 存储到本地(组件级别)状态,我们使用 editEntityRecord 操作将更新存储在 Redux 状态中。这是如何在浏览器的开发工具中尝试它的:
// We need a valid page ID to call editEntityRecord, so let's get the first available one using getEntityRecords.
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// Update the title
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', pageId, { title: 'updated title' } );
此时,您可能想知道 如何 editEntityRecord 比 useState 更好?答案是它提供了一些您否则无法获得的功能。
首先,我们可以像检索数据一样轻松保存更改,并确保所有缓存都会正确更新。
其次,通过 editEntityRecord 应用的更改可以通过 undo 和 redo 操作轻松撤销。
最后,由于更改存在于 Redux 状态中,它们是“全局”的,可以被其他组件访问。例如,我们可以让 PagesList 显示当前编辑的标题。
关于最后一点,让我们看看当我们使用 getEntityRecord 访问我们刚刚更新的实体记录时会发生什么:
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
它没有反映编辑。怎么了?
嗯,<PagesList /> 渲染 getEntityRecord() 返回的数据。如果 getEntityRecord() 反映了更新的标题,那么用户在 TextControl 中输入的任何内容也会立即显示在 <PagesList /> 中。这不是我们想要的。编辑不应在用户决定保存它们之前泄漏到表单之外。
Gutenberg Data 通过区分 实体记录 和 已编辑的实体记录 来解决这个问题。实体记录 反映来自 API 的数据并忽略任何本地编辑,而 已编辑的实体记录 还具有所有本地编辑应用在其上。两者同时存在于 Redux 状态中。
让我们看看如果我们调用 getEditedEntityRecord 会发生什么:
wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title
// "updated title"
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
// { "rendered": "<original, unchanged title>", "raw": "..." }
如您所见,实体记录的 title 是一个对象,而已编辑的实体记录的 title 是一个字符串。
这并非偶然。像 title、excerpt 和 content 这样的字段可能包含 短代码 或 动态块,这意味着它们只能在服务器上渲染。对于此类字段,REST API 同时提供 raw 标记和 rendered 字符串。例如,在块编辑器中,content.rendered 可用于视觉预览,而 content.raw 可用于填充代码编辑器。
那么为什么“已编辑实体记录”的 content 是一个字符串呢?由于 JavaScript 无法正确渲染任意的块标记,它仅存储不带 rendered 部分的 raw 标记。既然这是一个字符串,整个字段就变成了字符串。
我们现在可以相应地更新 EditPageForm。我们可以使用 useDispatch hook 来访问操作,类似于我们使用 useSelect 来访问选择器:
import { useDispatch } from '@wordpress/data';
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const page = useSelect(
select => select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
[ pageId ]
);
const { editEntityRecord } = useDispatch( coreDataStore );
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title } );
return (
<div className="my-gutenberg-form">
<TextControl
label="Page title:"
value={ page.title }
onChange={ handleChange }
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
Save
</Button>
<Button onClick={ onCancel } variant="tertiary">
Cancel
</Button>
</div>
</div>
);
}
我们添加了一个 onChange 处理器来通过 editEntityRecord action 跟踪编辑,然后将选择器更改为 getEditedEntityRecord,以便 page.title 始终反映更改。
现在的样子如下:

第 5 步:保存表单数据
现在我们可以编辑页面标题,让我们也确保可以保存它。在 Gutenberg 数据中,我们使用 saveEditedEntityRecord action 将更改保存到 WordPress REST API。它会发送请求,处理结果,并更新 Redux 状态中的缓存数据。
这是一个您可以在浏览器开发工具中尝试的示例:
// Replace 9 with an actual page ID
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', 9, { title: 'updated title' } );
wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'page', 9 );
上面的代码片段保存了一个新标题。与之前不同,getEntityRecord 现在反映了更新的标题:
// Replace 9 with an actual page ID
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ).title.rendered
// "updated title"
实体记录会在 REST API 请求完成后立即更新以反映任何保存的更改。
这就是带有功能 保存 按钮的 EditPageForm 的样子:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const { saveEditedEntityRecord } = useDispatch( coreDataStore );
const handleSave = () => saveEditedEntityRecord( 'postType', 'page', pageId );
return (
<div className="my-gutenberg-form">
{/* ... */}
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary">
Save
</Button>
{/* ... */}
</div>
</div>
);
}
它有效,但还有一个问题需要修复:表单模态框不会自动关闭,因为我们从未调用 onSaveFinished。幸运的是,saveEditedEntityRecord 返回一个 promise,在保存操作完成后解析。让我们在 EditPageForm 中利用它:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleSave = async () => {
await saveEditedEntityRecord( 'postType', 'page', pageId );
onSaveFinished();
};
// ...
}
第 6 步:处理错误
我们乐观地假设 保存 操作总是会成功。不幸的是,它可能会以多种方式失败:
- 网站可能已关闭
- 更新可能无效
- 页面可能已被其他人删除
为了在这些情况发生时通知用户,我们需要进行两项调整。我们不想在更新失败时关闭表单模态框。saveEditedEntityRecord 返回的 promise 仅在更新实际成功时才以更新的记录解析。当出现问题时,它会以空值解析。让我们利用它来保持模态框打开:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const handleSave = async () => {
const updatedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( updatedRecord ) {
onSaveFinished();
}
};
// ...
}
太好了!现在,让我们显示错误消息。可以使用 getLastEntitySaveError 选择器获取失败详情:
// Replace 9 with an actual page ID
wp.data.select( 'core' ).getLastEntitySaveError( 'postType', 'page', 9 )
以下是如何在 EditPageForm 中使用它:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const { lastError, page } = useSelect(
select => ({
page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId )
}),
[ pageId ]
)
// ...
return (
<div className="my-gutenberg-form">
{/* ... */}
{ lastError ? (
<div className="form-error">
Error: { lastError.message }
</div>
) : false }
{/* ... */}
</div>
);
}
太好了!EditPageForm 现在完全了解错误。
让我们看看这个错误消息的实际效果。我们将触发一个无效的更新并让它失败。帖子标题很难破坏,所以让我们将 date 属性设置为 -1——这是一个保证的验证错误:
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title, date: -1 } );
// ...
}
一旦您刷新页面,打开表单,更改标题并点击保存,您应该看到以下错误消息:

太棒了!我们现在可以恢复 handleChange 的先前版本,然后进入下一步。
第 7 步:状态指示器
我们的表单还有一个最后的问题:没有视觉反馈。我们无法确定 保存 按钮是否有效,直到表单消失或显示错误消息。
我们将澄清并向用户传达两种状态:保存中 和 未检测到更改。相关的选择器是 isSavingEntityRecord 和 hasEditsForEntityRecord。与 getEntityRecord 不同,它们从不发出任何 HTTP 请求,而只返回当前实体记录状态。
让我们在 EditPageForm 中使用它们:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const { isSaving, hasEdits, /* ... */ } = useSelect(
select => ({
isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
// ...
}),
[ pageId ]
)
}
我们现在可以使用 isSaving 和 hasEdits 在保存进行时显示加载动画,并在没有编辑时将保存按钮置灰:
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
return (
// ...
<div className="form-buttons">
<Button onClick={ handleSave } variant="primary" disabled={ ! hasEdits || isSaving }>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
// ...
);
}
请注意,当没有编辑内容且页面当前正在保存时,我们会禁用 保存 按钮。这是为了防止用户意外双击该按钮。
此外,中断正在进行的 保存 操作不受 @wordpress/data 支持,因此我们也条件性地禁用了 取消 按钮。
以下是实际运行效果:


将所有组件整合在一起
所有部分都已就位,太棒了!以下是本章中构建的所有内容汇总:
import { useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';
function PageEditButton( { pageId } ) {
const [ isOpen, setOpen ] = useState( false );
const openModal = () => setOpen( true );
const closeModal = () => setOpen( false );
return (
<>
<Button onClick={ openModal } variant="primary">
编辑
</Button>
{ isOpen && (
<Modal onRequestClose={ closeModal } title="编辑页面">
<EditPageForm
pageId={ pageId }
onCancel={ closeModal }
onSaveFinished={ closeModal }
/>
</Modal>
) }
</>
);
}
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const { page, lastError, isSaving, hasEdits } = useSelect(
( select ) => ( {
page: select( coreDataStore ).getEditedEntityRecord( 'postType', 'page', pageId ),
lastError: select( coreDataStore ).getLastEntitySaveError( 'postType', 'page', pageId ),
isSaving: select( coreDataStore ).isSavingEntityRecord( 'postType', 'page', pageId ),
hasEdits: select( coreDataStore ).hasEditsForEntityRecord( 'postType', 'page', pageId ),
} ),
[ pageId ]
);
const { saveEditedEntityRecord, editEntityRecord } = useDispatch( coreDataStore );
const handleSave = async () => {
const savedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( savedRecord ) {
onSaveFinished();
}
};
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', page.id, { title } );
return (
<div className="my-gutenberg-form">
<TextControl
label="页面标题:"
value={ page.title }
onChange={ handleChange }
/>
{ lastError ? (
<div className="form-error">错误:{ lastError.message }</div>
) : (
false
) }
<div className="form-buttons">
<Button
onClick={ handleSave }
variant="primary"
disabled={ ! hasEdits || isSaving }
>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
</div>
);
}