This commit is contained in:
2025-10-22 01:40:18 +08:00
parent 2080fa3878
commit 28ad1b3935
251 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
# 环境设置
我们将把应用程序构建为WordPress插件这意味着您需要先安装WordPress本体。其中一种安装方式是按照[快速入门](/docs/contributors/code/getting-started-with-code-contribution.md)页面的说明进行操作。完成环境配置后,您就可以继续学习本教程的后续内容。
另外本教程将大量涉及Redux相关概念例如状态state、操作actions和选择器selectors。如果您对这些概念不熟悉建议先阅读[Redux入门指南](https://redux.js.org/introduction/getting-started)。
## 创建插件
我们将在WordPress插件中完成所有开发工作。首先在本地WordPress环境的`wp-content/plugins/my-first-gutenberg-app`目录中创建以下四个文件:
- my-first-gutenberg-app.php - 用于创建新的管理页面
- src/index.js - 存放JavaScript应用程序代码
- src/style.css - 存放基础样式表
- package.json - 用于构建流程配置
请使用以下代码片段创建这些文件:
**src/index.js:**
```js
import { createRoot } from 'react-dom';
import './style.css';
function MyFirstApp() {
return <span>Hello from JavaScript!</span>;
}
const root = createRoot( document.getElementById( 'my-first-gutenberg-app' ) );
window.addEventListener(
'load',
function () {
root.render(
<MyFirstApp />,
);
},
false
);
```
**src/style.css:**
```css
.toplevel_page_my-first-gutenberg-app #wpcontent {
background: #fff;
height: 1000px;
}
button .components-spinner {
width: 15px;
height: 15px;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
}
.form-buttons {
display: flex;
}
.my-gutenberg-form .form-buttons {
margin-top: 20px;
margin-left: 1px;
}
.form-error {
color: #cc1818;
}
.form-buttons button {
margin-right: 4px;
}
.form-buttons .components-spinner {
margin-top: 0;
}
#my-first-gutenberg-app {
max-width: 500px;
}
#my-first-gutenberg-app ul,
#my-first-gutenberg-app ul li {
list-style-type: disc;
}
#my-first-gutenberg-app ul {
padding-left: 20px;
}
#my-first-gutenberg-app .components-search-control__input {
height: 36px;
margin-left: 0;
}
#my-first-gutenberg-app .list-controls {
display: flex;
width: 100%;
}
#my-first-gutenberg-app .list-controls .components-search-control {
flex-grow: 1;
margin-right: 8px;
}
```
**my-first-gutenberg-app.php:**
```php
<?php
/**
* Plugin Name: My first Gutenberg App
*
*/
function my_admin_menu() {
// 为我们的应用创建新的管理页面
add_menu_page(
__( 'My first Gutenberg app', 'gutenberg' ),
__( 'My first Gutenberg app', 'gutenberg' ),
'manage_options',
'my-first-gutenberg-app',
function () {
echo '
<h2>Pages</h2>
<div id="my-first-gutenberg-app"></div>
';
},
'dashicons-schedule',
3
);
}
add_action( 'admin_menu', 'my_admin_menu' );
function load_custom_wp_admin_scripts( $hook ) {
// 仅在 ?page=my-first-gutenberg-app 页面加载
if ( 'toplevel_page_my-first-gutenberg-app' !== $hook ) {
return;
}
// 加载必需的WordPress包
// 自动加载导入的依赖项和资源版本
$asset_file = include plugin_dir_path( __FILE__ ) . 'build/index.asset.php';
// 入队CSS依赖
foreach ( $asset_file['dependencies'] as $style ) {
wp_enqueue_style( $style );
}
// 加载我们的app.js
wp_register_script(
'my-first-gutenberg-app',
plugins_url( 'build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_script( 'my-first-gutenberg-app' );
// 加载我们的style.css
wp_register_style(
'my-first-gutenberg-app',
plugins_url( 'build/style-index.css', __FILE__ ),
array(),
$asset_file['version']
);
wp_enqueue_style( 'my-first-gutenberg-app' );
}
add_action( 'admin_enqueue_scripts', 'load_custom_wp_admin_scripts' );
```
**package.json:**
```json
{
"name": "09-code-data-basics-esnext",
"version": "1.1.0",
"private": true,
"description": "My first Gutenberg App",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"WordPress",
"block"
],
"homepage": "https://github.com/WordPress/gutenberg-examples/",
"repository": "git+https://github.com/WordPress/gutenberg-examples.git",
"bugs": {
"url": "https://github.com/WordPress/gutenberg-examples/issues"
},
"main": "build/index.js",
"devDependencies": {
"@wordpress/scripts": "^24.0.0"
},
"scripts": {
"build": "wp-scripts build",
"format": "wp-scripts format",
"lint:js": "wp-scripts lint-js",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start"
}
}
```
## 配置构建流程
本教程假设读者熟悉ESNext语法和构建工具如webpack的概念。如果这些概念让您感到困惑建议先阅读[JavaScript构建环境配置入门指南](/docs/how-to-guides/javascript/js-build-setup.md)。
要安装构建工具,请使用终端进入插件目录并运行`npm install`命令。
所有依赖项安装完成后,只需运行`npm start`即可!终端中将运行一个监听器。之后您可以在文本编辑器中随意编辑代码,每次保存后都会自动构建。
## 测试运行效果
现在进入插件页面,您应该能看到名为**My first Gutenberg App**的插件。请激活该插件此时会出现一个标为_My first Gutenberg app_的新菜单项。点击该菜单项后您将看到一个显示_Hello from JavaScript!_的页面
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/setup/hello-from-js.jpg)
恭喜!您现在可以开始构建应用程序了!
## 后续步骤
- 上一部分:[介绍](/docs/how-to-guides/data-basics/README.md)
- 下一部分:[构建基础页面列表](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md)
- 可选在block-development-examples仓库中查看[完整应用示例](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)

View File

@@ -0,0 +1,448 @@
### 整合所有模块
所有组件已就位太棒了以下是我们应用的完整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
);
```
现在只需刷新页面即可体验全新的状态指示器:
![搜索WordPress页面时显示的加载指示器](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/indicator.jpg)
![WordPress页面搜索未找到结果](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/no-results.jpg)
## 后续步骤
* **上一部分:** [环境设置](/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加载指示器
我们的搜索功能存在一个问题。我们无法完全确定它仍在搜索还是显示无结果:
![未找到匹配搜索条件的WordPress页面](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/unclear-status.jpg)
像“加载中...”或“无结果”这样的几条消息可以澄清状态。让我们来实现它们!首先,`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页面列表。本节完成后应用将呈现如下效果
![可搜索的WordPress页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg)
让我们逐步了解实现过程。
## 步骤一构建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页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/simple-list.jpg)
## 步骤二:获取数据
预设的示例页面并不实用。我们需要从[WordPress REST API](https://developer.wordpress.org/rest-api/)获取真实的页面列表。
首先请确保存在可获取的页面数据。在WPAdmin中通过侧边栏菜单进入“页面”栏目确认至少存在四到五个页面
![WordPress后台页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/pages-list.jpg)
若页面不足,请创建新页面(可参考上图所示标题),注意务必执行*发布*操作而非仅*保存*。
接下来我们使用[`@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实体`&aacute;`),需要使用[`decodeEntities`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-html-entities/)函数将其转换为对应符号(如`á`)。
刷新页面后将显示类似这样的列表:
![网站页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/fetch-the-data.jpg)
## 步骤三:转换为表格形式
```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>
);
}
```
![展示网站页面标题的表格](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/make-a-table.jpg)
## 步骤四:添加搜索框
当前页面列表虽然简短但随着内容增长操作会愈发困难。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/)组件。其实际效果如下:
![可搜索的WordPress页面列表](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter-field.jpg)
搜索框初始为空,输入内容会存储在`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>
)
}
```
大功告成!现在我们可以对结果进行筛选了:
![筛选后的WordPress页面列表显示'关于我们'](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter.jpg)

View File

@@ -0,0 +1,552 @@
## 接下来做什么?
* **上一篇:** [构建页面列表](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md)
* **下一篇:** [构建创建页面表单](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md)
* (可选)在 block-development-examples 代码库中查看[完整示例应用](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)
# 构建编辑表单
本节内容将为我们的应用添加*编辑*功能。以下是即将构建功能的预览图:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-finished.png)
### 步骤一:添加“编辑”按钮
要实现*编辑*功能首先需要添加编辑按钮,让我们从在`PagesList`组件中添加按钮开始:
```js
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
const PageEditButton = () => (
<Button variant="primary">
编辑
</Button>
)
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>
<td style={{width: 120}}>操作</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`组件中唯一的变化是新增了标为“操作”的列:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/edit-button.png)
### 步骤二:显示编辑表单
我们的按钮外观不错但尚未实现功能。要显示编辑表单,首先需要创建它:
```js
import { Button, TextControl } from '@wordpress/components';
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
return (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
value=''
label='页面标题:'
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
保存
</Button>
<Button onClick={ onCancel } variant="tertiary">
取消
</Button>
</div>
</div>
);
}
```
现在让按钮触发显示刚创建的编辑表单。由于本教程不侧重网页设计,我们将使用需要最少代码量的[`Modal`](https://developer.wordpress.org/block-editor/reference-guides/components/modal/)组件将两者连接。更新`PageEditButton`如下:
```js
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>
) }
</>
)
}
```
现在点击*编辑*按钮,您将看到如下模态框:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-scaffold.png)
很好!我们现在有了可操作的基础用户界面。
### 步骤三:在表单中填充页面详情
我们需要让`EditPageForm`显示当前编辑页面的标题。您可能注意到它并未接收`page`属性,仅接收`pageId`。这没有问题Gutenberg Data让我们能够轻松在任何组件中访问实体记录。
这里我们需要使用[`getEntityRecord`](/docs/reference-guides/data/data-core.md#getentityrecord)选择器。得益于`MyFirstApp`中的`getEntityRecords`调用记录列表已准备就绪甚至无需发起额外的HTTP请求——我们可以直接获取缓存记录。
您可以在浏览器开发工具中这样尝试:
```js
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ); // 将9替换为实际页面ID
```
接下来更新`EditPageForm`
```js
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
const page = useSelect(
select => select( coreDataStore ).getEntityRecord( 'postType', 'page', pageId ),
[pageId]
);
return (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label='页面标题:'
value={ page.title.rendered }
/>
{ /* ... */ }
</div>
);
}
```
现在效果应如图所示:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-populated.png)
### 步骤五:保存表单数据
既然我们已经能够编辑页面标题,接下来要确保能够保存它。在 Gutenberg 数据系统中,我们使用 `saveEditedEntityRecord` 操作将变更保存到 WordPress REST API。该操作会发送请求、处理结果并更新 Redux 状态中的缓存数据。
以下示例可在浏览器开发者工具中尝试:
```js
// 将数字9替换为实际页面ID
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', 9, { title: '更新后的标题' } );
wp.data.dispatch( 'core' ).saveEditedEntityRecord( 'postType', 'page', 9 );
```
以上代码片段保存了新标题。与之前不同,现在 `getEntityRecord` 会反映更新后的标题:
```js
// 将数字9替换为实际页面ID
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', 9 ).title.rendered
// 返回:"更新后的标题"
```
在 REST API 请求完成后,实体记录会立即更新以反映所有已保存的变更。
这是带有生效*保存*按钮的 `EditPageForm` 组件示例:
```js
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">
保存
</Button>
{/* ... */}
</div>
</div>
);
}
```
虽然功能已实现,但还需修复一个问题:表单模态框不会自动关闭,因为我们从未调用 `onSaveFinished`。幸运的是,`saveEditedEntityRecord` 返回的 Promise 会在保存操作完成后解析。让我们在 `EditPageForm` 中利用这个特性:
```js
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleSave = async () => {
await saveEditedEntityRecord( 'postType', 'page', pageId );
onSaveFinished();
};
// ...
}
```
### 步骤六:错误处理
此前我们乐观地假设*保存*操作总能成功。但实际操作可能因以下原因失败:
* 网站可能宕机
* 更新内容可能无效
* 页面可能已被他人删除
为了在出现这些问题时通知用户,我们需要进行两处调整。当更新失败时,我们不希望关闭表单模态框。仅当更新确实成功时,`saveEditedEntityRecord` 返回的 Promise 才会解析为更新后的记录。若出现异常,则会解析为空值。我们可以利用这一点来保持模态框开启状态:
```js
function EditPageForm( { pageId, onSaveFinished } ) {
// ...
const handleSave = async () => {
const updatedRecord = await saveEditedEntityRecord( 'postType', 'page', pageId );
if ( updatedRecord ) {
onSaveFinished();
}
};
// ...
}
```
很好!现在让我们来显示错误信息。可以通过 `getLastEntitySaveError` 选择器获取失败详情:
```js
// 将数字9替换为实际页面ID
wp.data.select( 'core' ).getLastEntitySaveError( 'postType', 'page', 9 )
```
以下是在 `EditPageForm` 中的具体应用:
```js
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">
错误{ lastError.message }
</div>
) : false }
{/* ... */}
</div>
);
}
```
太棒了!现在 `EditPageForm` 已能完全感知错误状态。
让我们通过触发无效更新来查看错误提示效果。由于文章标题很难引发错误,我们可以将 `date` 属性设置为 `-1` —— 这必定会触发验证错误:
```js
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
const handleChange = ( title ) => editEntityRecord( 'postType', 'page', pageId, { title, date: -1 } );
// ...
}
```
刷新页面后,打开表单修改标题并点击保存,您将看到如下错误提示:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-error.png)
非常好!现在我们可以**恢复 `handleChange` 函数的先前版本**,继续下一步操作。
### 步骤七:状态指示器
我们的表单还存在最后一个问题:缺乏视觉反馈。在表单消失或显示错误信息之前,我们无法完全确定*保存*按钮是否生效。
现在我们将解决这个问题并向用户传达两种状态_保存中_和_未检测到更改_。相关的选择器是`isSavingEntityRecord``hasEditsForEntityRecord`。与`getEntityRecord`不同这些选择器从不发起HTTP请求仅返回当前实体记录状态。
让我们在`EditPageForm`中使用它们:
```js
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`在保存过程中显示加载动画,并在无编辑内容时禁用保存按钮:
```js
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`不支持中断正在进行的*保存*操作,我们也相应禁用了*取消*按钮。
实际运行效果如下:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-inactive.png)
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-spinner.png)
### 整体联调
所有组件都已就位,太棒了!以下是我们本章构建的完整代码:
```js
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
__nextHasNoMarginBottom
__next40pxDefaultSize
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>
);
}
```
### 步骤四:实现页面标题字段的可编辑功能
我们的*页面标题*字段存在一个问题:无法编辑。它接收固定值但在输入时不会更新。我们需要一个 `onChange` 处理函数。
您可能在其他 React 应用中也见过类似的模式,这被称为["受控组件"](https://reactjs.org/docs/forms.html#controlled-components)
```js
function VanillaReactForm({ initialTitle }) {
const [title, setTitle] = useState( initialTitle );
return (
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
value={ title }
onChange={ setTitle }
/>
);
}
```
在 Gutenberg 数据中更新实体记录与此类似,但不同之处在于,我们不使用 `setTitle` 将数据存储在本地(组件级别)状态,而是使用 `editEntityRecord` 操作将更新存储在 *Redux* 状态中。以下是在浏览器的开发工具中尝试的方法:
```js
// 我们需要一个有效的页面 ID 来调用 editEntityRecord因此使用 getEntityRecords 获取第一个可用的 ID。
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// 更新标题
wp.data.dispatch( 'core' ).editEntityRecord( 'postType', 'page', pageId, { title: '更新后的标题' } );
```
此时,您可能会问,`editEntityRecord``useState` 好在哪里?答案是它提供了一些额外功能。
首先,我们可以像检索数据一样轻松地保存更改,并确保所有缓存都能正确更新。
其次,通过 `editEntityRecord` 应用的更改可以通过 `undo``redo` 操作轻松撤销或重做。
最后,由于更改存储在 *Redux* 状态中,它们是“全局的”,可以被其他组件访问。例如,我们可以让 `PagesList` 显示当前编辑的标题。
关于最后一点,让我们看看使用 `getEntityRecord` 访问刚刚更新的实体记录时会发生什么:
```js
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
```
它并未反映编辑后的内容。这是怎么回事?
实际上,`<PagesList />` 渲染的是 `getEntityRecord()` 返回的数据。如果 `getEntityRecord()` 反映了更新后的标题,那么用户在 `TextControl` 中输入的任何内容也会立即显示在 `<PagesList />` 中。这并不是我们想要的效果。在用户决定保存之前,编辑内容不应泄漏到表单之外。
Gutenberg 数据通过区分*实体记录*和*已编辑的实体记录*来解决这个问题。*实体记录*反映来自 API 的数据,忽略任何本地编辑,而*已编辑的实体记录*则在数据基础上应用了所有本地编辑。两者同时存在于 Redux 状态中。
让我们看看调用 `getEditedEntityRecord` 会发生什么:
```js
wp.data.select( 'core' ).getEditedEntityRecord( 'postType', 'page', pageId ).title
// "更新后的标题"
wp.data.select( 'core' ).getEntityRecord( 'postType', 'page', pageId ).title
// { "rendered": "<原始未更改的标题>", "raw": "..." }
```
如您所见,实体记录的 `title` 是一个对象,而已编辑实体记录的 `title` 是一个字符串。
这并非偶然。像 `title``excerpt``content` 这样的字段可能包含[短代码](https://developer.wordpress.org/apis/handbook/shortcode/)或[动态区块](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md)这意味着它们只能在服务器上渲染。对于这些字段REST API 同时暴露了 `raw` 标记和 `rendered` 字符串。例如,在区块编辑器中,`content.rendered` 可用于视觉预览,而 `content.raw` 可用于填充代码编辑器。
那么,为什么已编辑实体记录的 `content` 是一个字符串?由于 JavaScript 无法正确渲染任意的区块标记,它仅存储 `raw` 标记,而不包含 `rendered` 部分。由于这是一个字符串,整个字段就变成了字符串。
现在我们可以相应地更新 `EditPageForm`。我们可以使用 [`useDispatch`](/packages/data/README.md#usedispatch) 钩子访问操作,类似于使用 `useSelect` 访问选择器:
```js
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
__nextHasNoMarginBottom
__next40pxDefaultSize
label="页面标题:"
value={ page.title }
onChange={ handleChange }
/>
<div className="form-buttons">
<Button onClick={ onSaveFinished } variant="primary">
保存
</Button>
<Button onClick={ onCancel } variant="tertiary">
取消
</Button>
</div>
</div>
);
}
```
我们添加了一个 `onChange` 处理函数,通过 `editEntityRecord` 操作跟踪编辑,然后将选择器更改为 `getEditedEntityRecord`,以便 `page.title` 始终反映更改。
现在的效果如下:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/edit-form/form-editable.png)

View File

@@ -0,0 +1,395 @@
# 构建创建页面表单
在[上一章节](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)中我们创建了*编辑页面*功能,本章节我们将新增*创建页面*功能。以下是我们即将构建功能的预览:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/create-form-with-text.png)
### 步骤一:添加“创建新页面”按钮
首先我们构建一个用于显示创建页面表单的按钮,这与我们在[第三章节](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)构建的编辑按钮类似:
```js
import { useDispatch } from '@wordpress/data';
import { Button, Modal, TextControl } from '@wordpress/components';
function CreatePageButton() {
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="创建新页面">
<CreatePageForm
onCancel={ closeModal }
onSaveFinished={ closeModal }
/>
</Modal>
) }
</>
);
}
function CreatePageForm() {
// 暂时留空
return <div/>;
}
```
很好!现在让`MyFirstApp`显示我们全新的按钮:
```js
function MyFirstApp() {
// ...
return (
<div>
<div className="list-controls">
<SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
<CreatePageButton/>
</div>
<PagesList hasResolved={ hasResolved } pages={ pages }/>
</div>
);
}
```
最终效果如下所示:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/create-button.png)
### 步骤二:提取受控页面表单
按钮就位后,我们可以全力构建表单。本教程重点在于数据管理,因此不会构建完整的页面编辑器。表单将仅包含一个字段:文章标题。
幸运的是,我们在[第三章节](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)构建的`EditPageForm`已经实现了80%的功能。大部分用户界面已就绪,我们将在`CreatePageForm`中复用这些组件。首先将表单UI提取为独立组件
```js
function EditPageForm( { pageId, onCancel, onSaveFinished } ) {
// ...
return (
<PageForm
title={ page.title }
onChangeTitle={ handleChange }
hasEdits={ hasEdits }
lastError={ lastError }
isSaving={ isSaving }
onCancel={ onCancel }
onSave={ handleSave }
/>
);
}
function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
return (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label="页面标题:"
value={ title }
onChange={ onChangeTitle }
/>
{ lastError ? (
<div className="form-error">错误{ lastError.message }</div>
) : (
false
) }
<div className="form-buttons">
<Button
onClick={ onSave }
variant="primary"
disabled={ !hasEdits || isSaving }
>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
</div>
);
}
```
这段代码的质量优化不应改变应用程序的任何功能。让我们尝试编辑页面来确认:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/edit-page-form.png)
很好!编辑表单依然存在,现在我们有了构建新`CreatePageForm`的基础模块。
### 整合所有代码
以下是本章节构建的全部内容:
```js
function CreatePageForm( { onCancel, onSaveFinished } ) {
const [title, setTitle] = useState();
const { lastError, isSaving } = useSelect(
( select ) => ( {
lastError: select( coreDataStore )
.getLastEntitySaveError( 'postType', 'page' ),
isSaving: select( coreDataStore )
.isSavingEntityRecord( 'postType', 'page' ),
} ),
[]
);
const { saveEntityRecord } = useDispatch( coreDataStore );
const handleSave = async () => {
const savedRecord = await saveEntityRecord(
'postType',
'page',
{ title, status: 'publish' }
);
if ( savedRecord ) {
onSaveFinished();
}
};
return (
<PageForm
title={ title }
onChangeTitle={ setTitle }
hasEdits={ !!title }
onSave={ handleSave }
lastError={ lastError }
onCancel={ onCancel }
isSaving={ isSaving }
/>
);
}
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 (
<PageForm
title={ page.title }
onChangeTitle={ handleChange }
hasEdits={ hasEdits }
lastError={ lastError }
isSaving={ isSaving }
onCancel={ onCancel }
onSave={ handleSave }
/>
);
}
function PageForm( { title, onChangeTitle, hasEdits, lastError, isSaving, onCancel, onSave } ) {
return (
<div className="my-gutenberg-form">
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label="页面标题:"
value={ title }
onChange={ onChangeTitle }
/>
{ lastError ? (
<div className="form-error">错误{ lastError.message }</div>
) : (
false
) }
<div className="form-buttons">
<Button
onClick={ onSave }
variant="primary"
disabled={ !hasEdits || isSaving }
>
{ isSaving ? (
<>
<Spinner/>
保存中
</>
) : '保存' }
</Button>
<Button
onClick={ onCancel }
variant="tertiary"
disabled={ isSaving }
>
取消
</Button>
</div>
</div>
);
}
```
现在只需刷新页面即可体验表单功能:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/create-form-with-text.png)
## 后续步骤
* **下一章节:** [添加删除按钮](/docs/how-to-guides/data-basics/5-adding-a-delete-button.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)
### 步骤三构建CreatePageForm组件
`CreatePageForm`组件只需提供渲染`PageForm`组件所需的七个属性:
* 标题
* 标题变更处理函数
* 编辑状态标识
* 最后错误信息
* 保存状态标识
* 取消处理函数
* 保存处理函数
具体实现如下:
#### 标题、标题变更处理、编辑状态
`EditPageForm`组件更新并保存的是Redux状态中已存在的实体记录因此我们依赖`editedEntityRecords`选择器。
`CreatePageForm`不存在预先的实体记录只有空表单。用户输入的内容仅存在于本地表单可通过React的`useState`钩子进行跟踪:
```js
function CreatePageForm( { onCancel, onSaveFinished } ) {
const [title, setTitle] = useState();
const handleChange = ( title ) => setTitle( title );
return (
<PageForm
title={ title }
onChangeTitle={ setTitle }
hasEdits={ !!title }
{ /* 其他属性 */ }
/>
);
}
```
#### 保存处理、取消处理
`EditPageForm`中,我们通过`saveEditedEntityRecord('postType', 'page', pageId )`操作保存Redux状态中的编辑内容。
`CreatePageForm`既无Redux状态中的编辑内容也无pageId。此时需要调用的是[`saveEntityRecord`](https://developer.wordpress.org/block-editor/reference-guides/data/data-core/#saveentityrecord)操作名称中不含Edited它接收的是代表新实体记录的对象而非pageId。
传递给`saveEntityRecord`的数据会通过POST请求发送到对应REST API接口。例如执行以下操作
```js
saveEntityRecord( 'postType', 'page', { title: "测试页面" } );
```
将向[WordPress页面REST API接口](/wp/v2/pages)发起POST请求请求体中包含单个字段`title=测试页面`
现在我们将其应用到`CreatePageForm`
```js
function CreatePageForm( { onSaveFinished, onCancel } ) {
// ...
const { saveEntityRecord } = useDispatch( coreDataStore );
const handleSave = async () => {
const savedRecord = await saveEntityRecord(
'postType',
'page',
{ title }
);
if ( savedRecord ) {
onSaveFinished();
}
};
return (
<PageForm
{ /* 其他属性 */ }
onSave={ handleSave }
onCancel={ onCancel }
/>
);
}
```
还需注意:新建页面默认不会被`PagesList`获取。根据REST API文档`/wp/v2/pages`接口在创建POST请求时默认生成`status=draft`的页面但返回GET请求的是`status=publish`的页面。解决方案是显式传递status参数
```js
function CreatePageForm( { onSaveFinished, onCancel } ) {
// ...
const { saveEntityRecord } = useDispatch( coreDataStore );
const handleSave = async () => {
const savedRecord = await saveEntityRecord(
'postType',
'page',
{ title, status: 'publish' }
);
if ( savedRecord ) {
onSaveFinished();
}
};
return (
<PageForm
{ /* 其他属性 */ }
onSave={ handleSave }
onCancel={ onCancel }
/>
);
}
```
请将此更改应用到本地的`CreatePageForm`组件,接下来处理剩余两个属性。
#### 最后错误、保存状态
`EditPageForm`通过`getLastEntitySaveError``isSavingEntityRecord`选择器获取错误和进度信息,两者都传递三个参数:`( 'postType', 'page', pageId )`
`CreatePageForm`没有pageId参数。此时可省略pageId参数来获取未指定ID的实体记录信息即新建记录`useSelect`调用与`EditPageForm`非常相似:
```js
function CreatePageForm( { onCancel, onSaveFinished } ) {
// ...
const { lastError, isSaving } = useSelect(
( select ) => ( {
// 注意省略了pageId参数
lastError: select( coreDataStore )
.getLastEntitySaveError( 'postType', 'page' ),
// 注意省略了pageId参数
isSaving: select( coreDataStore )
.isSavingEntityRecord( 'postType', 'page' ),
} ),
[]
);
// ...
return (
<PageForm
{ /* 其他属性 */ }
lastError={ lastError }
isSaving={ isSaving }
/>
);
}
```
大功告成!以下是我们新表单的实际运行效果:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/create-saving.png)
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/create-form/created-item.png)

View File

@@ -0,0 +1,440 @@
## 下一步做什么?
* **上一部分:** [构建*创建页面表单*](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md)
* (可选)在 block-development-examples 代码库中查看[已完成的应用程序](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)
# 添加删除按钮
在[上一章节](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)中,我们实现了新建页面的功能,本章节将为应用添加*删除*功能。
以下是我们即将实现的效果预览:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/delete-button.png)
### 步骤一:添加删除按钮
首先创建 `DeletePageButton` 组件并更新 `PagesList` 组件的用户界面:
```js
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
const DeletePageButton = () => (
<Button variant="primary">
删除
</Button>
)
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>
<td style={{width: 190}}>操作</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 } />
{/* ↓ 这是 PagesList 组件中的唯一改动 */}
<DeletePageButton pageId={ page.id }/>
</div>
</td>
</tr>
) ) }
</tbody>
</table>
);
}
```
此时 PagesList 的显示效果如下:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/delete-button.png)
### 步骤二:为按钮绑定删除操作
在 Gutenberg 数据层中,我们通过 `deleteEntityRecord` 操作从 WordPress REST API 删除实体记录。该操作会发送请求、处理结果并更新 Redux 状态中的缓存数据。
以下是在浏览器开发者工具中尝试删除实体记录的方法:
```js
// 调用 deleteEntityRecord 需要有效的页面ID先通过 getEntityRecords 获取首个可用ID
const pageId = wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' )[0].id;
// 执行删除操作:
const promise = wp.data.dispatch( 'core' ).deleteEntityRecord( 'postType', 'page', pageId );
// 当 API 请求成功或失败时promise 会相应地被解析或拒绝
```
REST API 请求完成后,您会注意到列表中的某个页面已消失。这是因为列表通过 `useSelect()` 钩子和 `select( coreDataStore ).getEntityRecords( 'postType', 'page' )` 选择器动态生成。只要底层数据发生变化,列表就会立即使用新数据重新渲染,这非常便捷!
接下来让我们在点击 `DeletePageButton` 时触发该操作:
```js
const DeletePageButton = ({ pageId }) => {
const { deleteEntityRecord } = useDispatch( coreDataStore );
const handleDelete = () => deleteEntityRecord( 'postType', 'page', pageId );
return (
<Button variant="primary" onClick={ handleDelete }>
删除
</Button>
);
}
```
### 步骤三:添加视觉反馈
点击*删除*按钮后REST API 请求可能需要一些时间才能完成。与之前章节类似,我们可以通过 `<Spinner />` 组件来直观展示这一状态。
这里需要使用 `isDeletingEntityRecord` 选择器,它与[第三章节](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)中提到的 `isSavingEntityRecord` 选择器类似:返回 `true``false` 且不会触发任何 HTTP 请求:
```js
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 />
删除中...
</>
) : '删除' }
</Button>
);
}
```
实际运行效果如下:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/deleting-in-progress.png)
### 步骤4错误处理
我们之前乐观地假设*删除*操作总能成功。但不幸的是其底层是通过REST API发起的请求可能因多种原因失败
* 网站可能宕机
* 删除请求可能无效
* 页面可能已被其他用户删除
为了在发生这些错误时通知用户,我们需要使用`getLastEntityDeleteError`选择器提取错误信息:
```js
// 将9替换为实际页面ID
wp.data.select( 'core' ).getLastEntityDeleteError( 'postType', 'page', 9 )
```
以下是在`DeletePageButton`中的具体实现:
```js
import { useEffect } from 'react';
const DeletePageButton = ({ pageId }) => {
// ...
const { error, /* ... */ } = useSelect(
select => ( {
error: select( coreDataStore ).getLastEntityDeleteError( 'postType', 'page', pageId ),
// ...
} ),
[pageId]
);
useEffect( () => {
if ( error ) {
// 显示错误信息
}
}, [error] )
// ...
}
```
`error`对象来自`@wordpress/api-fetch`,包含以下错误属性:
* `message` 人类可读的错误信息(如`Invalid post ID`
* `code` 字符串型错误代码(如`rest_post_invalid_id`)。所有错误代码需参考[`/v2/pages`端点源码](https://github.com/WordPress/wordpress-develop/blob/2648a5f984b8abf06872151898e3a61d3458a628/src/wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php#L226-L230)
* `data`(可选)– 错误详情包含HTTP响应码的`code`属性
本教程将直接显示`error.message`来转换错误信息。
WordPress采用`Snackbar`组件显示状态信息,下图是**小工具编辑器**中的效果:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/snackbar-example.png)
现在为插件实现相同通知功能,包含两个部分:
1. 显示通知
2. 触发通知
#### 显示通知
当前应用只能显示页面需要新增通知显示功能。WordPress提供了完整的React通知组件其中[`Snackbar`组件](https://wordpress.github.io/gutenberg/?path=/story/components-snackbar--default)可呈现单条通知:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/snackbar.png)
不过我们不会直接使用`Snackbar`,而是采用能显示多条通知、支持平滑动画和自动隐藏的`SnackbarList`组件——这正是小工具编辑器和其他wp-admin页面使用的同款组件
创建自定义`Notifications`组件:
```js
import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
function Notifications() {
const notices = []; // 此处稍后完善
return (
<SnackbarList
notices={ notices }
className="components-editor-notices__snackbar"
/>
);
}
```
基础框架已搭建但当前通知列表为空。如何填充我们将沿用WordPress使用的[`@wordpress/notices`](https://github.com/WordPress/gutenberg/blob/895ca1f6a7d7e492974ea55f693aecbeb1d5bbe3/docs/reference-guides/data/data-core-notices.md)方案:
```js
import { SnackbarList } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
function Notifications() {
const notices = useSelect(
( select ) => select( noticesStore ).getNotices(),
[]
);
const { removeNotice } = useDispatch( noticesStore );
const snackbarNotices = notices.filter( ({ type }) => type === 'snackbar' );
return (
<SnackbarList
notices={ snackbarNotices }
className="components-editor-notices__snackbar"
onRemove={ removeNotice }
/>
);
}
function MyFirstApp() {
// ...
return (
<div>
{/* ... */}
<Notifications />
</div>
);
}
```
本教程重点在于页面管理,不深入讨论上述代码细节。若想了解`@wordpress/notices`的详细信息,建议查阅[手册页面](https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/)。
现在我们已经准备好向用户报告可能发生的错误了。
#### 发送通知
有了 SnackbarNotices 组件后,我们现在可以发送通知了!具体操作如下:
```js
import { useEffect } from 'react';
import { store as noticesStore } from '@wordpress/notices';
function DeletePageButton( { pageId } ) {
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
// 如果传入存储句柄而非回调函数useSelect 将返回选择器列表:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// 告知用户操作成功:
createSuccessNotice( "页面已删除!", {
type: 'snackbar',
} );
} else {
// 在 deleteEntityRecord 执行失败后,直接使用选择器获取最新错误信息
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || '出现错误。' ) + ' 请刷新页面后重试。'
// 向用户明确展示操作失败原因:
createErrorNotice( message, {
type: 'snackbar',
} );
}
}
// ...
}
```
太好了!现在 `DeletePageButton` 已完全具备错误感知能力。让我们看看实际错误提示效果。通过将 `pageId` 乘以一个大数值来触发无效删除操作:
```js
function DeletePageButton( { pageId, onCancel, onSaveFinished } ) {
pageId = pageId * 1000;
// ...
}
```
刷新页面并点击任意 `删除` 按钮后,您将看到如下错误提示:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/snackbar-error.png)
完美!现在可以**移除 `pageId = pageId * 1000;` 这行代码**。
接下来尝试实际删除页面。刷新浏览器并点击删除按钮后,您将看到:
![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/delete-button/snackbar-success.png)
大功告成!
### 完整功能集成
所有组件已就绪,太棒了!以下是本章节完成的所有代码变更:
```js
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 (
<div>
<div className="list-controls">
<SearchControl onChange={ setSearchTerm } value={ searchTerm }/>
<PageCreateButton/>
</div>
<PagesList hasResolved={ hasResolved } pages={ pages }/>
<Notifications />
</div>
);
}
function SnackbarNotices() {
const notices = useSelect(
( select ) => select( noticesStore ).getNotices(),
[]
);
const { removeNotice } = useDispatch( noticesStore );
const snackbarNotices = notices.filter( ( { type } ) => type === 'snackbar' );
return (
<SnackbarList
notices={ snackbarNotices }
className="components-editor-notices__snackbar"
onRemove={ removeNotice }
/>
);
}
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>
<td style={ { width: 190 } }>操作</td>
</tr>
</thead>
<tbody>
{ pages?.map( ( page ) => (
<tr key={ page.id }>
<td>{ page.title.rendered }</td>
<td>
<div className="form-buttons">
<PageEditButton pageId={ page.id }/>
<DeletePageButton pageId={ page.id }/>
</div>
</td>
</tr>
) ) }
</tbody>
</table>
);
}
function DeletePageButton( { pageId } ) {
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
// 如果传入存储句柄而非回调函数useSelect 将返回选择器列表:
const { getLastEntityDeleteError } = useSelect( coreDataStore )
const handleDelete = async () => {
const success = await deleteEntityRecord( 'postType', 'page', pageId);
if ( success ) {
// 告知用户操作成功:
createSuccessNotice( "页面已删除!", {
type: 'snackbar',
} );
} else {
// 此时直接使用选择器获取错误信息
// 假设我们通过以下方式获取错误:
// const { lastError } = useSelect( function() { /* ... */ } );
// 那么 lastError 在 handleDelete 内部将显示为 null
// 为什么?因为这里引用的是在 handleDelete 被调用前就计算好的旧版本
const lastError = getLastEntityDeleteError( 'postType', 'page', pageId );
const message = ( lastError?.message || '出现错误。' ) + ' 请刷新页面后重试。'
// 向用户明确展示操作失败原因:
createErrorNotice( message, {
type: 'snackbar',
} );
}
}
const { deleteEntityRecord } = useDispatch( coreDataStore );
const { isDeleting } = useSelect(
select => ( {
isDeleting: select( coreDataStore ).isDeletingEntityRecord( 'postType', 'page', pageId ),
} ),
[ pageId ]
);
return (
<Button variant="primary" onClick={ handleDelete } disabled={ isDeleting }>
{ isDeleting ? (
<>
<Spinner />
删除中...
</>
) : '删除' }
</Button>
);
}
```

View File

@@ -0,0 +1,15 @@
# 使用Gutenberg数据创建你的首个应用
本教程旨在帮助你熟悉Gutenberg数据层。它将指导你构建一个简单的React应用程序让用户能够管理他们的WordPress页面。完成后的应用效果如下
[![在WordPress Playground中打开演示](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/WordPress/block-development-examples/trunk/plugins/data-basics-59c8f8/_playground/blueprint.json "在WordPress Playground中打开演示")
你可以在block-development-examples代码库中查看[完整应用](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)。
### 目录
1. [环境设置](/docs/how-to-guides/data-basics/1-data-basics-setup.md)
2. [构建基础页面列表](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md)
3. [构建编辑表单](/docs/how-to-guides/data-basics/3-building-an-edit-form.md)
4. [构建创建页面表单](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md)
5. [添加删除按钮](/docs/how-to-guides/data-basics/5-adding-a-delete-button.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB