first commit

This commit is contained in:
2025-10-22 01:33:45 +08:00
commit 2080fa3878
251 changed files with 47081 additions and 0 deletions

45
how-to-guides/README.md Normal file
View File

@@ -0,0 +1,45 @@
# 操作指南
与WordPress大多数功能一样新版编辑器具有高度灵活性。您可以创建自定义区块、修改编辑器外观、添加特殊插件等众多功能。
## 创建区块
编辑器的核心是区块主要扩展接口是区块API。通过该接口您可以创建静态区块、[动态区块](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md)(在服务器端渲染),以及能够将数据保存至文章元数据的区块,从而实现更具结构化的内容呈现。
若想深入了解区块创建,请参阅[创建区块教程](/docs/getting-started/devenv/get-started-with-create-block.md),这是最佳入门指南。
## 扩展区块
通过过滤器可以修改现有区块的行为,甚至完全移除区块。
更多内容请参阅[区块过滤器](/docs/reference-guides/filters/block-filters.md)章节。
特别针对「查询循环」区块,除了现有过滤器外,还有更多扩展方式和定制版本创建方法。详见[扩展查询循环区块](/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md)章节。
## 扩展编辑器界面
通过`registerPlugin`接口可扩展编辑器界面,允许您集中定义所有插件界面元素。
更多信息请参考[插件](/packages/plugins/README.md)和[编辑文章](/packages/edit-post/README.md)章节。
您还可以对编辑器的特定功能进行过滤,相关文档请参阅[编辑器过滤器](/docs/reference-guides/filters/editor-filters.md)页面。
## 元数据框
强烈建议将PHP元数据框迁移至区块或侧边栏插件具体方法请参阅[元数据框](/docs/how-to-guides/metabox.md)与[侧边栏插件](/docs/how-to-guides/plugin-sidebar-0.md)指南。
## 主题支持
默认情况下,区块会提供样式以确保在未修改的主题中正常显示。主题可以添加/覆盖这些样式,或直接使用默认样式。
部分高级区块功能需要主题主动声明支持。请参阅[主题支持](/docs/how-to-guides/themes/theme-support.md)与[全局样式过滤指南](/docs/reference-guides/filters/global-styles-filters.md)。
## 自动补全
区块内的自动补全功能可进行扩展和重写。了解更多关于[自动补全](/docs/reference-guides/filters/autocomplete-filters.md)过滤器的信息。
## 区块解析与序列化
编辑器中的文章在存储至`post_content`与呈现在编辑器之间会经历多个处理阶段。由于区块本身是内存中的数据结构,需要通过解析与序列化步骤实现与数据库存储格式的相互转换。
自定义解析器属于高级主题,您可以在[扩展解析器](/docs/reference-guides/filters/parser-filters.md)章节深入了解。

View File

@@ -0,0 +1,16 @@
# 可访问性
为参与古腾堡项目开发的工程师提供的无障碍功能文档。
有关WordPress无障碍功能的更多信息请参阅[WordPress无障碍功能手册](https://make.wordpress.org/accessibility/handbook/)及[无障碍功能团队专区](https://make.wordpress.org/accessibility/)。
## 地标区域
最佳实践是将页面所有内容都包含在地标区域内,这样依赖地标区域在版块间导航的屏幕阅读器用户就不会遗漏内容。
关于设置不同区域间的导航功能,请参阅[navigateRegions组件包](/packages/components/src/higher-order/navigate-regions/README.md)获取详细文档。
更多关于地标设计的W3C标准
- [地标设计通用原则](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/#x4-2-general-principles-of-landmark-design)
- [ARIA地标区域范例](https://www.w3.org/WAI/ARIA/apg/example-index/landmarks/)
- [默认定义ARIA地标的HTML5元素](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/#x4-1-html-sectioning-elements)

View File

@@ -0,0 +1,11 @@
# 区块
本教程旨在逐步讲解创建新区块类型的基础知识。从最简单的示例开始,每个新章节将逐步扩展内容,涵盖您在实现自己的区块类型时可能需要使用的常见功能。
要跟随本教程学习,您可以下载[配套的 WordPress 插件](https://github.com/WordPress/block-development-examples),其中包含所有示例供您在自己的站点上尝试。在每个步骤中,可以通过修改示例来实践自己的想法,并观察它们对区块行为产生的影响。
> 要获取最新版本的 .zip 文件,请前往代码库的[发布页面](https://github.com/WordPress/block-development-examples/releases),在最新发布的「资源」部分查找。
代码片段提供两种格式「JSX」和「原生」。JSX 指使用 JSX 语法的 JavaScript 代码需要构建步骤。原生指无需构建的「经典」JavaScript。您可以通过每个代码示例上方的选项卡切换这两种格式。使用 JSX 需要您运行 [JavaScript 构建步骤](/docs/how-to-guides/javascript/js-build-setup/),将代码编译为浏览器兼容的格式。
请注意,创建区块或扩展编辑器并不强制要求使用 JSX您可以使用经典 JavaScript。然而一旦熟悉了 JSX 和构建步骤,许多开发者往往会发现它更易于阅读和编写,因此您看到的大多数代码示例都使用 JSX 语法。

View File

@@ -0,0 +1,163 @@
# 使用样式与样式表
## 概述
区块通常会在文章内容中插入需要特定样式化的标记HTML。本指南将详细介绍几种在区块编辑器中使用 CSS 的不同方法,以及如何处理样式和样式表。
## 开始之前
您需要准备一个基础区块和 WordPress 开发环境来实现本指南中的示例。请参阅[快速入门指南](/docs/getting-started/quick-start-guide.md)或[区块教程](/docs/getting-started/tutorial.md)完成环境配置。
## 添加样式的方法
以下是为区块添加样式的不同方法(适用于编辑器界面或保存后的显示效果):
## 方法一:内联样式
第一种方法演示了如何添加内联样式。这种方式会将定义的样式转换为插入元素的属性。
通过 `useBlockProps` React 钩子可以设置并应用区块包装元素的属性。具体示例如下:
```jsx
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-02-stylesheets', {
edit() {
const greenBackground = {
backgroundColor: '#090',
color: '#fff',
padding: '20px',
};
const blockProps = useBlockProps( { style: greenBackground } );
return (
<p { ...blockProps }>Hello World编辑器界面显示绿色背景</p>
);
},
save() {
const redBackground = {
backgroundColor: '#900',
color: '#fff',
padding: '20px',
};
const blockProps = useBlockProps.save( { style: redBackground } );
return (
<p { ...blockProps }>Hello World前端显示红色背景</p>
);
},
} );
```
## 方法二:区块类名
内联样式适用于少量 CSS 的情况。如果需要更复杂的样式,使用独立的样式表文件会更便于管理。
`useBlockProps` 钩子会自动包含区块的类名,它会根据区块名称生成以 `wp-block-` 为前缀的类名,并将命名空间分隔符 `/` 替换为 `-`
例如区块名称 `gutenberg-examples/example-02-stylesheets` 对应的类名为:`wp-block-gutenberg-examples-example-02-stylesheets`。虽然较长但能有效避免与其他区块冲突。
```jsx
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-02-stylesheets', {
edit() {
const blockProps = useBlockProps();
return (
<p { ...blockProps }>Hello World编辑器界面显示绿色背景</p>
);
},
save() {
const blockProps = useBlockProps.save();
return (
<p { ...blockProps }>Hello World前端显示红色背景</p>
);
},
} );
```
### 构建或添加依赖
如需将 blockEditor 添加为依赖项,请确保执行构建步骤或更新资源 PHP 文件。
构建脚本并更新用于跟踪依赖关系和构建版本的资源文件:
```bash
npm run build
```
### 加载样式表
与脚本类似,您可以通过 `block.json` 文件加载区块的样式表。
- `editorStyle` 属性:指定仅在编辑器界面加载的 CSS 文件
- `style` 属性:指定在编辑器界面和前端都会加载的 CSS 文件
- `viewStyle` 属性:指定仅在前端加载的 CSS 文件(当区块被使用时)
值得注意的是,如果编辑器内容位于 iframe 中,`style``editorStyle` 都会在 iframe 内加载。`editorStyle` 还会在 iframe 外加载,因此可用于编辑器内容和界面样式。
示例:
```json
{
"apiVersion": 3,
"name": "gutenberg-examples/example-02-stylesheets",
"title": "示例:样式表",
"icon": "universal-access-alt",
"category": "layout",
"editorScript": "file:./block.js",
"editorStyle": "file:./editor.css",
"style": "file:./style.css"
}
```
在插件目录中创建 `editor.css` 文件用于编辑器界面:
```css
/* 绿色背景 */
.wp-block-gutenberg-examples-example-02-stylesheets {
background: #090;
color: white;
padding: 20px;
}
```
创建 `style.css` 文件用于前端显示:
```css
/* 红色背景 */
.wp-block-gutenberg-examples-example-02-stylesheets {
background: #900;
color: white;
padding: 20px;
}
```
在 block.json 中指定后,这些文件将自动加载。
<div class="callout callout-info">
如果使用 `@wordpress/scripts`,需要在对应的 JavaScript 文件中导入样式表,以便 `@wordpress/scripts` 处理样式表。
示例:
-`edit.js` 中添加 `import './editor.scss';`
-`index.js` 中添加 `import './style.scss';`
-`view.js` 中添加 `import './view.scss';`(交互式区块模板)
</div>
**注意:** 如需加载多个文件,可以像其他插件或主题一样使用标准的 `wp_enqueue_style` 函数。区块编辑器建议使用以下钩子:
- `enqueue_block_editor_assets` - 仅在编辑器界面加载
- `enqueue_block_assets` - 在前端和编辑器界面同时加载
## 总结
本指南展示了通过内联样式或独立样式表为区块应用样式的不同方法。这两种方法都使用了 `useBlockProps` 钩子,更多细节请参阅[区块包装器参考文档](/docs/reference-guides/block-api/block-edit-save.md#block-wrapper-props)。
完整代码请查看[区块开发示例库](https://github.com/WordPress/block-development-examples)中的 [stylesheets-79a4c3](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/stylesheets-79a4c3) 示例。

View File

@@ -0,0 +1,137 @@
# 创建动态区块
动态区块是在前端渲染时实时构建结构和内容的区块。
动态区块主要有两个用途:
1. **内容需要实时更新的区块**即使文章未更新区块内容也会自动变化。WordPress 自带的「最新文章」区块就是典型示例——每当发布新文章时,所有使用该区块的位置都会同步更新。
2. **需要即时呈现代码更新的区块**当修改区块代码HTML/CSS/JS使用动态区块可确保修改立即在全站所有该区块实例上生效。若不使用动态区块Gutenberg 的[验证流程](/docs/reference-guides/block-api/block-edit-save.md#validation)会触发,导致用户看到“此区块似乎已被外部修改”的提示)
对于多数动态区块,`save` 回调函数应返回 `null`,这会让编辑器仅将[区块属性](/docs/reference-guides/block-api/block-attributes.md)保存至数据库。这些属性随后会传入服务端渲染回调,从而由开发者决定如何在前端展示区块。返回 `null` 时,编辑器会跳过区块标记验证流程,避免因频繁变化的标记引发问题。
若在动态区块中使用 [InnerBlocks](/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md),需通过 `<InnerBlocks.Content/>``save` 回调中保存内部区块内容。
也可保存区块的 HTML 表示形式。若设置了服务端渲染回调,该 HTML 会被回调输出替换;但当区块被停用或渲染回调被移除时,此 HTML 仍会正常渲染。
区块属性可用于保存任何需要存储的内容或设置。例如最新文章区块中,可将要显示的文章数量保存为属性;又或者针对需要在前端展示的每项内容(如标题文本、段落文字、图片、链接等),都可通过属性进行配置。
以下代码示例展示了如何创建仅显示最新文章链接的动态区块:
```jsx
import { registerBlockType } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-dynamic', {
apiVersion: 3,
title: '示例:最新文章',
icon: 'megaphone',
category: 'widgets',
edit: () => {
const blockProps = useBlockProps();
const posts = useSelect( ( select ) => {
return select( 'core' ).getEntityRecords( 'postType', 'post' );
}, [] );
return (
<div { ...blockProps }>
{ ! posts && '加载中' }
{ posts && posts.length === 0 && '暂无文章' }
{ posts && posts.length > 0 && (
<a href={ posts[ 0 ].link }>
{ posts[ 0 ].title.rendered }
</a>
) }
</div>
);
},
} );
```
由于是动态区块,无需在客户端重写默认的 `save` 实现,但需要配置服务端组件。前端显示内容取决于 `register_block_type``render_callback` 属性所调用的函数。
```php
<?php
/**
* 插件名称Gutenberg 动态区块示例
*/
function gutenberg_examples_dynamic_render_callback( $block_attributes, $content ) {
$recent_posts = wp_get_recent_posts( array(
'numberposts' => 1,
'post_status' => 'publish',
) );
if ( count( $recent_posts ) === 0 ) {
return '暂无文章';
}
$post = $recent_posts[ 0 ];
$post_id = $post['ID'];
return sprintf(
'<a class="wp-block-my-plugin-latest-post" href="%1$s">%2$s</a>',
esc_url( get_permalink( $post_id ) ),
esc_html( get_the_title( $post_id ) )
);
}
function gutenberg_examples_dynamic() {
// 自动加载依赖项与版本号
$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
wp_register_script(
'gutenberg-examples-dynamic',
plugins_url( 'build/block.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version']
);
register_block_type( 'gutenberg-examples/example-dynamic', array(
'api_version' => 3,
'editor_script' => 'gutenberg-examples-dynamic',
'render_callback' => 'gutenberg_examples_dynamic_render_callback'
) );
}
add_action( 'init', 'gutenberg_examples_dynamic' );
```
需要注意以下几点:
- `edit` 函数仍在编辑器中展示区块预览(可与渲染版本完全不同,由区块作者决定)
- 内置 `save` 函数直接返回 `null`,因为渲染在服务端完成
- 服务端渲染函数接收区块属性及内部内容作为参数,返回标记(与短代码机制高度相似)
**注意:** 关于颜色、边框、间距等常见自定义设置,我们将在[下一章](/docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md)了解如何通过区块支持功能高效实现。
## 在区块编辑器中实时渲染
Gutenberg 2.8 版本引入了 [`<ServerSideRender>`](/packages/server-side-render/README.md) 区块,支持在服务端使用 PHP 进行渲染而非 JavaScript。
*服务端渲染仅作为降级方案,始终推荐使用客户端 JavaScript 渲染(速度更快且支持更灵活的编辑器操作)。*
```jsx
import { registerBlockType } from '@wordpress/blocks';
import ServerSideRender from '@wordpress/server-side-render';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-dynamic', {
apiVersion: 3,
title: '示例:最新文章',
icon: 'megaphone',
category: 'widgets',
edit: function ( props ) {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<ServerSideRender
block="gutenberg-examples/example-dynamic"
attributes={ props.attributes }
/>
</div>
);
},
} );
```
注意此代码使用了 `wp-server-side-render` 包但未使用 `wp-data`,请确保在 PHP 代码中更新依赖项。建议使用 wp-scripts 自动构建依赖(具体 PHP 代码配置可参考 [block-development-examples 代码库](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/basic-esnext-a2ab62))。

View File

@@ -0,0 +1,286 @@
### 在前端实现自定义查询功能
查询循环区块主要通过文章模板区块运作,该区块接收属性并据此构建查询。查询循环区块的其他一级子元素(如分页区块)也以相同方式工作。它们构建查询后,通过 [`query_loop_block_query_vars`](https://developer.wordpress.org/reference/hooks/query_loop_block_query_vars/) 过滤器暴露结果。
您可以通过挂接该过滤器来修改查询。请务必确保不会对其他查询循环区块产生副作用,至少需要检查仅将过滤器应用于您的变体!
```php
if( 'my-plugin/books-list' === $block[ 'attrs' ][ 'namespace' ] ) {
add_filter(
'query_loop_block_query_vars',
function( $query ) {
/** 在此处读取区块的自定义查询参数并构建查询 */
},
);
}
```
(上述代码中,我们假设您有某种访问区块的方式,例如通过 [`pre_render_block`](https://developer.wordpress.org/reference/hooks/pre_render_block/) 过滤器,但具体解决方案可能因使用场景而异,因此这并非硬性建议)。
### 在编辑器端实现自定义查询功能
要完成自定义变体,我们可能希望编辑器能响应自定义查询的更改,并相应显示适当的预览。虽然这对区块功能并非必需,但能为区块使用者提供完全集成的用户体验。
查询循环区块通过 [WordPress REST API](https://developer.wordpress.org/rest-api/) 获取文章以显示预览。任何添加到 `query` 对象的额外参数都将作为查询参数传递给 API。这意味着这些额外参数要么需要 REST API 支持,要么需要通过自定义过滤器处理,例如 [`rest_{$this->post_type}_query`](https://developer.wordpress.org/reference/hooks/rest_this-post_type_query/) 过滤器,该过滤器允许您介入自定义文章类型的任何 API 请求。如下所示:
```php
add_filter(
'rest_book_query',
function( $args, $request ) {
/** 我们可以从这里访问自定义参数 */
$book_author = $request->get_param( 'bookAuthor' );
/** ...您的自定义查询逻辑 */
}
);
```
就这样,您就创建了一个功能完整的查询循环区块变体!
# 扩展查询循环区块
查询循环区块是一个功能强大的工具,允许用户循环遍历指定的文章列表,并显示一系列将继承列表中每篇文章上下文的区块。例如,可以将其设置为循环遍历特定分类下的所有文章,并为每篇文章显示其特色图像。当然,功能远不止于此!
但正因为查询循环区块功能强大且支持高度自定义,它也可能令人望而生畏。大多数用户不希望面对查询循环区块的全部功能,因为他们可能不熟悉“查询”概念及其相关技术术语。相反,大多数用户可能更青睐预设版本的区块,具有更少的设置项和更清晰的命名。默认提供的“文章列表”变体就是这种做法的良好示例:用户可以在不接触技术细节的情况下使用查询循环区块,同时也更有可能发现并理解该区块的用途。
同样地,许多扩展开发者可能需要提供定制化区块版本的方法,这些版本具有自己的预设配置、附加设置,并剔除与其使用场景无关的自定义选项(例如,通常针对其自定义文章类型)。查询循环区块提供了非常强大的方式来创建此类变体。
## 通过变体扩展区块
通过注册具有特定查询循环区块设置的自定义区块变体,您可以更精细地控制其呈现方式,同时仍能使用查询循环区块底层提供的全部功能。如果您不熟悉区块变体,可在此处了解更多信息。
通过区块变体API您可以为您的使用场景提供最合理的默认设置。
要使查询循环变体正常工作,我们需要:
-`core/query`区块注册具有某些默认值的区块变体
- 为区块变体定义布局
-`isActive`区块变体属性中使用`namespace`属性
让我们以为一个注册了`book`自定义文章类型的插件设置变体为例,开始这段旅程。
### 1. 提供合理的默认值
您的第一步是创建一个变体,该变体将默认显示书籍列表而非博客文章。完整的变体代码如下所示:
```js
const MY_VARIATION_NAME = 'my-plugin/books-list';
registerBlockVariation( 'core/query', {
name: MY_VARIATION_NAME,
title: '书籍列表',
description: '显示书籍列表',
isActive: ( { namespace, query } ) => {
return (
namespace === MY_VARIATION_NAME
&& query.postType === 'book'
);
},
icon: /** 此处可放置SVG图标 */,
attributes: {
namespace: MY_VARIATION_NAME,
query: {
perPage: 6,
pages: 0,
offset: 0,
postType: 'book',
order: 'desc',
orderBy: 'date',
author: '',
search: '',
exclude: [],
sticky: '',
inherit: false,
},
},
scope: [ 'inserter' ],
}
);
```
如果这看起来很多,请不要担心,让我们逐一查看这里的属性,了解它们的存在意义和作用。
本质上,您可以从这样的代码开始:
```js
registerBlockVariation( 'core/query', {
name: 'my-plugin/books-list',
attributes: {
query: {
/** ...更多查询设置(如需要) */
postType: 'book',
},
},
} );
```
通过这种方式,用户无需从下拉菜单中选择自定义的`postType`,即可直接获得正确的配置。但您可能会问,用户如何找到并插入这个变体?好问题!要实现这一点,您应该添加:
```js
{
/** ...变体属性 */
scope: [ 'inserter' ],
}
```
这样,当用户在编辑器中搜索时,您的区块将像任何其他区块一样显示。此时,您可能还想为变体添加自定义图标、标题和描述,如下所示:
```js
{
/** ...变体属性 */
title: '书籍列表',
description: '显示书籍列表',
icon: /* 您的SVG图标在此 */,
}
```
至此,您的自定义变体将与独立区块几乎无法区分。完全为您的插件品牌化,易于发现,并作为即插即用的选项直接提供给用户。
然而,您的查询循环变体尚不能正常工作——我们仍需定义布局以便其正确渲染。
## 扩展查询功能
即便具备上述所有功能,您的自定义文章类型可能仍有独特需求:可能需要支持某些用于筛选和查询的自定义属性,或者某些查询参数可能无关紧要甚至完全不支持!我们在设计查询循环区块时已考虑到这类使用场景,下面来看看如何解决这个问题。
### 禁用无关或不支持的查询控件
假设您的图书数据完全未使用`sticky`属性,那么该设置对区块定制将毫无意义。为避免用户困惑并呈现清晰的交互界面,我们需要隐藏此控件。再假设您完全未使用`author`字段(该字段通常表示将文章添加到数据库的用户),而是使用自定义的`bookAuthor`字段。此时若保留`author`筛选器不仅会造成混淆,更会直接"破坏"您的查询。
为此,查询循环区块变体支持名为`allowedControls`的属性,该属性接收数组参数用于指定要在检查器侧边栏显示的控件键值。默认情况下会显示所有控件,但一旦设置该属性,我们就需要明确指定相关控件!
截至Gutenberg 14.2版本,可用控件如下:
- `inherit` - 显示切换开关,允许查询直接从模板继承
- `postType` - 显示可用文章类型下拉菜单
- `order` - 显示查询排序方式下拉菜单
- `sticky` - 显示置顶文章处理方式下拉菜单
- `taxQuery` - 显示当前所选文章类型的分类法筛选器
- `author` - 显示按作者筛选查询的输入字段
- `search` - 显示按关键词筛选查询的输入字段
- `format` - 显示按[格式集合](https://developer.wordpress.org/advanced-administration/wordpress/post-formats/#supported-formats)筛选查询的输入字段
- `parents` - 显示使用父实体筛选查询的输入字段
在本案例中,该属性配置如下:
```js
{
/** ...变体属性 */
allowedControls: [ 'inherit', 'order', 'taxQuery', 'search' ],
}
```
如需隐藏所有可用控件,可将`allowedControls`值设为空数组。
注意我们同时禁用了`postType`控件。当用户选择我们的变体时,为何还要显示令人困惑的文章类型下拉菜单?更重要的是,正如稍后将介绍的,这可能会破坏我们实现自定义控件的区块功能。
### 添加额外控件
由于我们的插件需要使用自定义属性进行查询,我们需要添加专属控件来替代刚刚从核心检查器禁用的控件。这可以通过接入[区块过滤器](https://developer.wordpress.org/block-editor/reference-guides/filters/block-filters/)的[React HOC](https://reactjs.org/docs/higher-order-components.html)实现:
```jsx
import { InspectorControls } from '@wordpress/block-editor';
export const withBookQueryControls = ( BlockEdit ) => ( props ) => {
// 仅当属于我们的变体时才添加这些控件
// 此处可实现自定义逻辑进行验证,类似于前述的`isActive`函数
// 以下假设您已编写自定义的`isMyBooksVariation`函数进行处理
return isMyBooksVariation( props ) ? (
<>
<BlockEdit key="edit" { ...props } />
<InspectorControls>
<BookAuthorSelector /> { /** 我们的自定义组件 */ }
</InspectorControls>
</>
) : (
<BlockEdit key="edit" { ...props } />
);
};
addFilter( 'editor.BlockEdit', 'core/query', withBookQueryControls );
```
当然,您需要自行实现控件的逻辑(建议参考[`@wordpress/components`](https://www.npmjs.com/package/@wordpress/components)使控件完美融入Gutenberg界面。通过少量额外工作您在区块属性的`query`对象内设置的任何额外参数,都可用于创建符合需求的自定义查询。
当前您可能需要实现略有差异的路径,以确保在前端(即最终用户侧)正确执行查询,并在编辑器端显示正确的预览。
```js
{
/** ...变体属性 */
attributes: {
/** ...变体属性 */
query: {
/** ...更多查询设置(如需要) */
postType: 'book',
/** 我们的自定义查询参数 */
bookAuthor: 'J. R. R. Tolkien'
}
}
}
```
### 2. 自定义变体布局
请注意,查询循环区块支持在 `scope` 属性中使用字符串 `'block'`。理论上,这是为了让变体在插入区块后被识别。更多关于区块变体选择器的信息请[参阅此处](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-variation-picker/README.md)。
然而,目前**不建议**使用此方式,这是因为查询循环通过模式和 `scope: [ 'block' ]` 变体进行设置时,所选模式的所有属性将被使用,但 `postType``inherit` 查询属性除外,这可能导致冲突和变体功能异常。
为解决此问题,有两种方法。第一种是添加默认的 `innerBlocks`,如下所示:
```js
innerBlocks: [
[
'core/post-template',
{},
[ [ 'core/post-title' ], [ 'core/post-excerpt' ] ],
],
[ 'core/query-pagination' ],
[ 'core/query-no-results' ],
],
```
通过在变体中包含 `innerBlocks`,您实际上跳过了查询循环区块通过建议模式进行设置的阶段,区块将以这些内部区块作为初始内容插入。
另一种方法是为您的变体注册特定模式,这些模式将在设置过程中被推荐,并替换区块的流程。
查询循环区块会判断自身是否有活跃变体,以及该变体是否有特定模式可用。如果有,这些模式将是唯一推荐给用户的模式,不会包含原始查询循环区块的默认模式。否则,如果没有此类模式,将推荐默认模式。
若要将模式与查询循环变体“关联”,您应在模式的 `blockTypes` 属性中添加以查询循环名称前缀的变体名称(例如 `core/query/$variation_name`)。有关注册模式的更多详情,请[参阅此处](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/)。
如果您未在变体中提供 `innerBlocks`,还有一种方法可以在用户于设置阶段选择 `Start blank` 时推荐“关联”变体。这与“关联”模式的处理方式类似,通过检查查询循环是否有活跃变体以及是否有任何关联变体可推荐。
若要将一个变体与另一个查询循环变体关联,我们需要将 `scope` 属性定义为 `['block']`,并将 `namespace` 属性定义为一个数组。该数组应包含希望关联的变体的名称(`name` 属性)。
例如,如果我们有一个名称为 `products` 的查询循环变体暴露于插入器中(`scope: ['inserter']`),我们可以通过将其 `namespace` 属性设置为 `['products']` 来关联一个作用域为 `block` 的变体。如果用户在点击 `Start blank` 后选择此变体,`namespace` 属性将被主要的插入器变体覆盖。
### 3. 让 Gutenberg 识别您的变体
在实现此变体后,您可能意识到一个小问题:虽然用户在插入时对此无感知,但 Gutenberg 仍会将变体识别为查询循环区块的核心功能,因此在插入后,例如在编辑器的树形视图中,它将显示为查询循环区块。
我们需要一种方式告诉编辑器,此区块实际上是您的特定变体。这正是 `isActive` 属性的用途:它是一种基于区块属性判断特定变体是否活跃的方法。您可以像这样使用它:
```js
{
/** ...变体属性 */
isActive: ( { namespace, query } ) => {
return (
namespace === MY_VARIATION_NAME
&& query.postType === 'book'
);
},
}
```
您可能倾向于仅比较 `postType`,以便当 `postType` 匹配 `book`Gutenberg 会将区块识别为您的变体。然而,这种方式范围过广,因为其他插件可能也希望基于 `book` 文章类型发布变体,或者我们可能不希望用户在编辑器设置中手动将类型设置为 `book` 时每次都识别为变体。
这就是为什么查询循环区块暴露了一个名为 `namespace` 的特殊属性。它在区块实现中实际上没有任何作用,而是作为一种简单且一致的方式供扩展者识别和限定自己的变体。此外,`isActive` 还可以接受一个字符串数组,用于比较属性。通常,`namespace` 就足够了,您可以这样使用:
```js
{
/** ...变体属性 */
attributes: {
/** ...变体属性 */
namespace: 'my-plugin/books-list',
},
isActive: [ 'namespace' ],
}
```
这样Gutenberg 只有在匹配您的自定义命名空间时才会知道这是您的特定变体!非常方便!

View File

@@ -0,0 +1,278 @@
## 使用 React 钩子
你可以使用名为 `useInnerBlocksProps` 的 React 钩子来替代 `InnerBlocks` 组件。该钩子能让你更灵活地控制内部区块区域的标记结构。
`useInnerBlocksProps``InnerBlocks` 组件同样从 `@wordpress/block-editor` 包中导出,并支持该组件的所有功能。其工作方式类似于 `useBlockProps` 钩子。
需特别注意:必须在调用 `useInnerBlocksProps` *之前* 调用 `useBlockProps` 钩子,否则 `useBlockProps` 将返回空对象。
以下是基础的 `useInnerBlocksProps` 钩子用法:
```js
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-06', {
// ...
edit: () => {
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps();
return (
<div { ...blockProps }>
<div {...innerBlocksProps} />
</div>
);
},
save: () => {
const blockProps = useBlockProps.save();
const innerBlocksProps = useInnerBlocksProps.save();
return (
<div { ...blockProps }>
<div {...innerBlocksProps} />
</div>
);
},
} );
```
该钩子还可将 `useBlockProps` 钩子返回的对象传递给 `useInnerBlocksProps` 钩子,从而减少需要创建的元素数量:
```js
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-06', {
// ...
edit: () => {
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps( blockProps );
return (
<div {...innerBlocksProps} />
);
},
save: () => {
const blockProps = useBlockProps.save();
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
return (
<div {...innerBlocksProps} />
);
},
} );
```
上述代码将在编辑器中渲染为以下标记结构:
```html
<div>
<!-- 内部区块将插入在此处 -->
</div>
```
使用钩子方案的另一个优势是:可以利用返回值(仅为一个对象)并通过解构获取其中的 React 子元素。该属性包含实际的子内部区块,因此我们可以将元素放置在与内部区块同一层级:
```js
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-06', {
// ...
edit: () => {
const blockProps = useBlockProps();
const { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps );
return (
<div {...innerBlocksProps}>
{ children }
<!-- 可在此处插入与子元素同层级的任意HTML -->
</div>
);
},
// ...
} );
```
```html
<div>
<!-- 内部区块将插入在此处 -->
<!-- 自定义HTML将渲染于同一层级 -->
</div>
```
## 在区块中使用父级、祖先与子级关系
使用 InnerBlocks 的常见模式是创建一个仅在其父级区块插入时才可用的自定义区块。这使构建者能够在建立区块间关系的同时,限制嵌套区块的可发现性。构建者可使用三种关系类型:`parent`(父级)、`ancestor`(祖先)和 `allowedBlocks`(允许的区块)。它们的区别在于:
- 若指定 `parent`则表示嵌套区块仅能作为__父级的直接后代__被使用和插入
- 若指定 `ancestor`则表示嵌套区块仅能作为__父级的后代__被使用和插入
- 若指定 `allowedBlocks`则表示反向关系——即哪些区块可以作为__此区块的直接后代__被使用和插入
`parent``ancestor` 的核心区别在于:`parent` 具有更精确的特定性,而 `ancestor` 在嵌套层级中具有更高灵活性。
### 定义父级区块关系
典型案例是 Column区块它被设置了 `parent` 区块配置。这使得 Column 区块仅能作为其父级 Columns列组区块的嵌套直接后代使用。否则该区块将不会在区块插入器中显示。参阅 [Column 代码实现](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/column)。
定义直接后代区块时,需使用 `parent` 区块配置来声明父级区块。这能防止嵌套区块在其定义的 InnerBlock 之外显示于插入器中。
```json
{
"title": "栏目",
"name": "core/column",
"parent": [ "core/columns" ],
// ...
}
```
### 定义祖先区块关系
典型案例是 Comment Author Name评论作者名称区块它被设置了 `ancestor` 区块配置。这使得该区块仅能作为其祖先 Comment Template评论模板区块的嵌套后代使用。否则该区块将不会在区块插入器中显示。参阅 [Comment Author Name 代码实现](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/comment-author-name)。
通过 `ancestor` 关系Comment Author Name 区块可以存在于层级树中的任意位置而__不仅限于__父级 Comment Template 区块的直接子级,同时仍能限制其在区块插入器中的可见性——仅当 Comment Template 区块存在时才显示为可插入选项。
定义后代区块时,需使用 `ancestor` 区块配置。这能防止嵌套区块在其定义的 InnerBlock 之外显示于插入器中。
```json
{
"title": "评论作者名称",
"name": "core/comment-author-name",
"ancestor": [ "core/comment-template" ],
// ...
}
```
### 定义子级区块关系
典型案例是 Navigation导航区块它被设置了 `allowedBlocks` 区块配置。这使得仅特定类型的区块能作为 Navigation 区块的直接后代使用。参阅 [Navigation 代码实现](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/navigation)。
自定义区块构建者可扩展 `allowedBlocks` 设置。通过挂载 `blocks.registerBlockType` 过滤器,自定义区块可将自身添加到 Navigation 的可用子级列表中。
定义可能的子代区块集合时,需使用 `allowedBlocks` 区块配置。这会限制插入新子区块时插入器中显示的区块类型。
```json
{
"title": "导航",
"name": "core/navigation",
"allowedBlocks": [ "core/navigation-link", "core/search", "core/social-links", "core/page-list", "core/spacer" ],
// ...
}
```
# 嵌套区块:使用 InnerBlocks
您可以使用 [InnerBlocks](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md) 组件创建包含其他区块的独立区块。这一技术被广泛应用于分栏区块、社交链接区块等需要容纳其他区块的组件中。
注意:单个区块只能包含一个 `InnerBlocks` 组件。
以下是 InnerBlocks 的基本用法:
```js
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'gutenberg-examples/example-06', {
// ...
edit: () => {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<InnerBlocks />
</div>
);
},
save: () => {
const blockProps = useBlockProps.save();
return (
<div { ...blockProps }>
<InnerBlocks.Content />
</div>
);
},
} );
```
## 允许的区块
通过 `allowedBlocks` 属性,您可以在 `block.json``allowedBlocks` 字段基础上,进一步限制哪些区块可以作为该区块的直接子级插入。这对于针对每个区块动态确定允许的区块列表非常实用,例如根据区块属性来决定:
```js
const { allowedBlocks } = attributes;
//...
<InnerBlocks allowedBlocks={ allowedBlocks } />;
```
如果允许的区块列表始终保持不变,建议改用 [`allowedBlocks` 区块设置](#定义子级区块关系)。
## 排列方向
默认情况下,`InnerBlocks` 假定其包含的区块以垂直列表形式呈现。在某些应用场景中,可能需要通过为内部区块包装器添加 CSS 弹性布局或网格属性,使内部区块水平排列。当区块采用此类样式时,可通过设置 `orientation` 属性来指示当前正在使用水平布局:
```js
<InnerBlocks orientation="horizontal" />
```
设置此属性不会影响内部区块的布局,但会使子区块中的区块移动器图标水平显示,同时确保拖放功能正常工作。
## 默认区块
默认情况下,当点击区块添加器时,`InnerBlocks` 会通过 `allowedBlocks` 显示允许的区块列表。您可以使用 `defaultBlock` 属性来修改点击初始区块添加器时插入的默认区块及其属性。例如:
```js
<InnerBlocks defaultBlock={['core/paragraph', {placeholder: "Lorem ipsum..."}]} directInsert />
```
默认情况下,此功能处于禁用状态,除非将 `directInsert` 属性设置为 `true`。这使您能够指定默认区块是否应该插入的条件。
## 模板
使用 template 属性可以定义一组区块,在 InnerBlocks 组件没有现有内容时预填充内容。您可以为这些区块设置属性来定义其用途。以下示例展示了使用 InnerBlocks 组件设置书评模板,并通过占位符值展示区块用途:
```js
const MY_TEMPLATE = [
[ 'core/image', {} ],
[ 'core/heading', { placeholder: '书名' } ],
[ 'core/paragraph', { placeholder: '内容摘要' } ],
];
//...
edit: () => {
return (
<InnerBlocks
template={ MY_TEMPLATE }
templateLock="all"
/>
);
},
```
使用 `templateLock` 属性可以锁定模板。设置为 `all` 时将完全锁定模板,禁止任何修改;设置为 `insert` 时则仅允许重新排序现有区块,禁止插入新区块。更多信息请参阅 [templateLock 文档](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock)。
### 文章模板
虽然与 `InnerBlocks` 无直接关联,但值得在此说明:您可以按文章类型创建[文章模板](https://developer.wordpress.org/block-editor/developers/block-api/block-templates/),使区块编辑器预加载一组特定区块。
`InnerBlocks` 模板仅适用于您创建的单个区块组件,而文章的其余部分可以包含用户喜欢的任何区块。使用文章模板则可以将整篇文章锁定为您定义的模板结构。
```php
add_action( 'init', function() {
$post_type_object = get_post_type_object( 'post' );
$post_type_object->template = array(
array( 'core/image' ),
array( 'core/heading' )
);
} );
```

View File

@@ -0,0 +1,25 @@
# 优化编辑器使用体验
优化WordPress编辑器体验至关重要它能让您简化编辑流程确保内容与网站风格及品牌指南保持一致。同时这也能让用户更轻松高效地创建和管理内容避免意外修改或布局变动从而打造更高效、更个性化的使用体验。
本指南旨在介绍多种锁定和优化WordPress使用体验的方法特别是在引入更多设计工具和站点编辑器之后。
在本节中,您将了解:
1. [**区块锁定**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/block-locking):如何通过限制用户在编辑器中对特定区块的交互来实现更好的内容控制
2. [**模式**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/patterns):关于创建和实现预定义区块布局以确保设计与内容的统一性
3. [**theme.json**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/theme-json)通过theme.json文件配置主题的全局样式和设置
4. [**过滤器和钩子**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/filters-and-hooks):用于修改编辑器的核心过滤器和钩子
5. [**禁用编辑器功能**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/disable-editor-functionality):选择性禁用编辑器功能或组件的其他方法,以优化用户体验
## 组合运用多种方法
请注意,上述文档中提供的方法可以根据需要组合使用。例如,您可以在创建新页面时提供自定义模式,同时限制对这些模式的某些方面进行自定义,比如仅允许对封面区块背景使用特定的预设颜色,或锁定可删除的区块。
在考虑采用哪些方法时,请思考您希望以何种具体方式既开放使用体验又进行精心管控。
## 扩展资源
- [构建器基础——全站编辑中的模板运用(第三部分)](https://wordpress.tv/2022/05/24/nick-diego-builder-basics-working-with-templates-in-full-site-editing-part-3/)
- [核心编辑器改进通过锁定API和theme.json实现定制化体验](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/)
- [关于优化编辑器体验的WordPress学习课程](https://wordpress.tv/2022/07/22/nick-diego-curating-the-editor-experience/)

View File

@@ -0,0 +1,69 @@
# 区块锁定 API
区块锁定 API 允许您限制编辑器内特定区块的操作。该 API 可用于防止用户移动、删除或编辑某些区块,从而确保布局一致性和内容完整性。
## 锁定移动或删除特定区块的功能
用户可通过编辑器锁定和解锁区块。锁定界面提供防止区块在内容画布内移动或被删除的选项:
![锁定界面示意图](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20interface.png?raw=true)
请注意,通过启用"应用于内部所有区块"选项,您可以将锁定选项应用于容器区块内的嵌套区块。但除此之外,无法批量锁定区块。
## 锁定编辑特定区块的功能
除了锁定移动或删除区块的功能外,[导航区块](https://github.com/WordPress/gutenberg/pull/44739)和[可重用区块](https://github.com/WordPress/gutenberg/pull/39950)还具备额外功能:锁定编辑区块内容的能力。这将禁止对这两种区块类型内部的任何区块进行修改。
## 对模式或模板应用区块锁定
在构建模式或模板时,主题作者可以使用相同的界面工具设置区块的默认锁定状态。例如,主题作者可以锁定页眉的不同组成部分。请注意,默认情况下具有编辑权限的用户可以解锁这些区块。[这里有一个包含不同锁定方式区块的模式示例](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805),以及关于[创建包含锁定区块的模板](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/)的更多背景信息。您可以在编辑器内直接构建这些模式(包括添加锁定选项),然后按照[注册文档](/docs/reference-guides/block-api/block-patterns.md)进行操作。
## 在模式或模板中应用仅内容编辑
此功能在 WordPress 6.1 中引入。与禁用移动或删除区块功能的区块锁定不同,仅内容编辑功能专为模式或模板层级设计,它会隐藏所有设计工具,同时仍允许编辑区块内容。这为简化用户界面和保留设计提供了绝佳方案。启用该选项后会发生以下变化:
- 非内容子区块(容器、间距器、列等)将从列表视图隐藏,在画布上不可点击,且完全不可编辑。
- 检查器将显示所有"内容"子区块列表,点击列表中的区块可显示其设置面板。
- 主列表视图仅显示内容区块,所有区块均处于同一层级(忽略实际嵌套关系)。
- 整体内容锁定容器内的子区块会自动启用移动/删除锁定。
- 无法插入额外子区块,进一步保护设计和布局。
- 区块工具栏设有"修改"链接,用户可切换此选项来获取更全面的设计工具。目前无法通过编程方式移除该选项。
该选项可应用于 Columns、Cover 和 Group 区块,以及在其 block.json 中具有 templateLock 属性的第三方区块。要启用此功能,需使用 `"templateLock":"contentOnly"`。[此处是具备此功能的模式示例](https://gist.github.com/annezazu/d62acd2514cea558be6cea97fe28ff3c)。更多信息请[查阅相关文档](/docs/reference-guides/block-api/block-templates.md#locking)。
注意:目前没有管理内容锁定的可视化界面,必须通过代码层级进行管理。
## 更改权限以控制锁定能力
机构和插件开发者可通过限制用户[锁定和解锁区块的权限](https://make.wordpress.org/core/2022/05/05/block-locking-settings-in-wordpress-6-0/),提供更精细的定制体验。默认情况下,所有管理员都具备锁定和解锁区块的权限。
开发者可通过向 [block_editor_settings_all](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) 钩子添加过滤器来配置区块锁定权限。该钩子向回调函数传递两个参数:
- `$settings` - 编辑器的可配置设置数组
- `$context` - WP_Block_Editor_Context 实例,包含当前编辑器信息的对象
具体而言,开发者可通过设置 `$settings['canLockBlocks']` 值为 `true``false` 来修改权限,通常需要执行一个或多个条件判断。
以下示例在编辑页面时为所有用户禁用区块锁定权限:
```php
add_filter( 'block_editor_settings_all', function( $settings, $context ) {
if ( $context->post && 'page' === $context->post->post_type ) {
$settings['canLockBlocks'] = false;
}
return $settings;
}, 10, 2 );
```
另一种常见使用场景是仅允许可编辑网站视觉设计(主题编辑)的用户锁定或解锁区块。目前最佳方案是通过检测 `edit_theme_options` 权限来实现,如下列代码片段所示:
```php
add_filter( 'block_editor_settings_all', function( $settings ) {
$settings['canLockBlocks'] = current_user_can( 'edit_theme_options' );
return $settings;
} );
```
开发者可使用任何类型的条件判断来确定锁定/解锁区块的权限。这仅是通过过滤钩子实现功能的冰山一角。

View File

@@ -0,0 +1,125 @@
# 禁用编辑器功能
本文专门介绍在文章编辑器和站点编辑器中禁用特定功能的多种方法,这些内容未包含在管理文档的其他部分。
## 限制区块选项
有时您可能希望完全禁止用户使用某些区块。要控制插入器中可用的内容,您可以采用两种方法:[允许列表](/docs/reference-guides/filters/block-filters.md#using-an-allow-list)(仅启用列表中的区块)或[拒绝列表](/docs/reference-guides/filters/block-filters.md#using-a-deny-list)(取消注册特定区块)。
## 管理标题层级
具有标题层级下拉菜单的WordPress核心区块支持`levelOptions`属性。这适用于标题、站点标题、站点标语、查询标题、文章标题和评论标题区块。`levelOptions`属性接受对应标题层级的数字数组,其中`1`代表H1`2`代表H2依此类推。
此属性允许您指定哪些标题层级应出现在下拉UI中提供了一种轻量级的管理方法无需弃用区块。现有标题层级会在标记中保留`levelOptions`仅影响UI显示。
您可以直接在区块标记中应用此属性,这种技术将常用于区块模板、模板部件和模式。例如,以下标记通过设置`"levelOptions":[3,4,5]`在标题区块中禁用H1、H2和H6。
```html
<!-- wp:heading {"level":3,"levelOptions":[3,4,5],"className":"wp-block-heading"} -->
<h3 class="wp-block-heading">标记示例</h3>
<!-- /wp:heading -->
```
您也可以使用[区块过滤器](/docs/reference-guides/filters/block-filters.md)全局或为特定区块设置此属性的默认值。以下示例为所有标题区块禁用H1、H2和H6。您可以通过基于用户权限等条件限制特定标题层级来进一步自定义。
```php
function example_modify_heading_levels_globally( $args, $block_type ) {
if ( 'core/heading' !== $block_type ) {
return $args;
}
// 移除H1、H2和H6
$args['attributes']['levelOptions']['default'] = [ 3, 4, 5 ];
return $args;
}
add_filter( 'register_block_type_args', 'example_modify_heading_levels_globally', 10, 2 );
```
## 禁用模式目录
要从插入器中完全移除WordPress核心捆绑的模式可将以下代码添加到您的`functions.php`文件:
```php
function example_theme_support() {
remove_theme_support( 'core-block-patterns' );
}
add_action( 'after_setup_theme', 'example_theme_support' );
```
## 禁用区块变体
某些核心区块实际上是[区块变体](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/)。一个很好的例子是行和堆栈区块,它们实际上是群组区块的变体。如果您想禁用这些"区块",实际上需要禁用相应的变体。
区块变体使用JavaScript注册需要使用JavaScript禁用。以下代码将禁用行变体。
```js
wp.domReady( () => {
wp.blocks.unregisterBlockVariation( 'core/group', 'group-row' );
});
```
假设代码放置在主题文件夹根目录的`disable-variations.js`文件中,您可以使用以下代码在主题的`functions.php`中加载此文件。
```php
function example_disable_variations_script() {
wp_enqueue_script(
'example-disable-variations-script',
get_template_directory_uri() . '/disable-variations.js',
array( 'wp-dom-ready' ),
wp_get_theme()->get( 'Version' ),
true
);
}
add_action( 'enqueue_block_editor_assets', 'example_disable_variations_script' );
```
## 禁用区块样式
有几个核心区块包含自己的[区块样式](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/)。例如图像区块包含一个名为"圆形"的圆角图像样式。您可能不希望用户使用圆形图像,或者更倾向于使用边框半径控件而非区块样式。无论哪种情况,都可以轻松禁用不需要的区块样式。
与区块变体不同您可以使用JavaScript或PHP注册样式。如果样式是用JavaScript注册的必须用JavaScript禁用。如果使用PHP注册可以用任一方法禁用样式。所有核心区块样式都是用JavaScript注册的。
因此,您可以使用以下代码禁用图像区块的"圆形"样式。
```js
wp.domReady( () => {
wp.blocks.unregisterBlockStyle( 'core/image', 'rounded' );
});
```
此JavaScript的加载方式与上面的区块变体示例类似。有关使用PHP注册和取消注册样式的方法请参阅[区块样式](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/)文档。
## 禁用代码编辑器访问权限
代码编辑器允许您查看页面或文章的底层区块标记。虽然此视图对有经验的用户很方便,但编辑内容可能会意外破坏区块标记。将以下代码添加到`functions.php`文件以限制访问。
```php
function example_restrict_code_editor_access( $settings, $context ) {
$settings[ 'codeEditingEnabled' ] = false;
return $settings;
}
add_filter( 'block_editor_settings_all', 'example_restrict_code_editor_access', 10, 2 );
```
此代码阻止所有用户访问代码编辑器。您也可以添加[权限](https://wordpress.org/documentation/article/roles-and-capabilities/)检查来为特定用户禁用访问。
## 禁用富文本区块的格式化选项
支持[富文本](https://developer.wordpress.org/block-editor/reference-guides/richtext/)的区块附带WordPress提供的默认格式化选项。
需要使用`unregisterFormatType`通过JavaScript禁用格式化选项。以下代码将全局禁用内联图像、语言、键盘输入、下标和上标选项。
```js
wp.domReady( () => {
wp.richText.unregisterFormatType( 'core/image' );
wp.richText.unregisterFormatType( 'core/language' );
wp.richText.unregisterFormatType( 'core/keyboard' );
wp.richText.unregisterFormatType( 'core/subscript' );
wp.richText.unregisterFormatType( 'core/superscript' );
});
```
此JavaScript的加载方式与上面的区块变体示例类似。

View File

@@ -0,0 +1,178 @@
## 更多资源
- [如何使用服务器端过滤器修改 theme.json 数据](https://developer.wordpress.org/news/2023/07/05/how-to-modify-theme-json-data-using-server-side-filters/)WordPress 开发者博客)
- [通过客户端过滤器定制编辑器体验](https://developer.wordpress.org/news/2023/05/24/curating-the-editor-experience-with-client-side-filters/)WordPress 开发者博客)
## 客户端(编辑器)过滤器
WordPress 6.2 引入了一个全新的客户端过滤器,允许您在编辑器渲染前修改区块层级的 [theme.json 设置](/docs/reference-guides/theme-json-reference/theme-json-living.md#settings)。
该过滤器名为 `blockEditor.useSetting.before`,可通过以下方式在 JavaScript 代码中使用:
```js
import { addFilter } from '@wordpress/hooks';
/**
* 将栏目区块的间距选项限制为像素单位。
*/
addFilter(
'blockEditor.useSetting.before',
'example/useSetting.before',
( settingValue, settingName, clientId, blockName ) => {
if ( blockName === 'core/column' && settingName === 'spacing.units' ) {
return [ 'px' ];
}
return settingValue;
}
);
```
此示例将把栏目区块的可用间距单位限制为仅像素单位。如前所述,类似的限制也可以通过 theme.json 过滤器或直接在主题的 theme.json 文件中使用区块层级设置来实现。
`blockEditor.useSetting.before` 过滤器的独特之处在于,它允许您根据区块位置、相邻区块、当前用户角色等条件动态修改设置,定制可能性极为丰富。
下面这个示例展示了当标题区块被放置在「媒体与文本」区块内部时,会自动禁用其文字颜色控制功能:
```js
import { select } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
/**
* 当标题区块置于媒体与文本区块内时,禁用其文字颜色控制功能。
*/
addFilter(
'blockEditor.useSetting.before',
'example/useSetting.before',
( settingValue, settingName, clientId, blockName ) => {
if ( blockName === 'core/heading' ) {
const { getBlockParents, getBlockName } = select( 'core/block-editor' );
const blockParents = getBlockParents( clientId, true );
const inMediaText = blockParents.some( ( ancestorId ) => getBlockName( ancestorId ) === 'core/media-text' );
if ( inMediaText && settingName === 'color.text' ) {
return false;
}
}
return settingValue;
}
);
```
## 区块过滤器
除了优化编辑器界面外,您还可以通过多种方式修改单个区块。比如禁用特定区块支持功能(如背景色),或定义特定区块默认显示的设置项。
其中最常用的过滤器之一是 [`block_type_metadata`](https://developer.wordpress.org/reference/hooks/block_type_metadata/)。当区块类型在服务器端通过 PHP 注册时,该过滤器允许您拦截从区块 `block.json` 文件加载的原始元数据。
该过滤器接收一个参数:
- `$metadata` (`array`) `block.json` 加载的区块类型注册元数据。
`$metadata` 数组包含您可能需要了解的所有区块信息,从描述和[属性](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/)到区块[支持功能](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/)。
以下示例演示了如何禁用标题区块的背景色与渐变支持:
```php
function example_disable_heading_background_color_and_gradients( $metadata ) {
// 仅对标题区块应用此过滤器
if ( ! isset( $metadata['name'] ) || 'core/heading' !== $metadata['name'] ) {
return $metadata;
}
// 检查是否存在支持功能配置
if ( isset( $metadata['supports'] ) && isset( $metadata['supports']['color'] ) ) {
// 移除背景色与渐变支持
$metadata['supports']['color']['background'] = false;
$metadata['supports']['color']['gradients'] = false;
}
return $metadata;
}
add_filter( 'block_type_metadata', 'example_disable_heading_background_color_and_gradients' );
```
您可以通过[区块过滤器文档](https://developer.wordpress.org/block-editor/reference-guides/filters/block-filters/)了解更多可用过滤器。
# 过滤器与钩子
编辑器提供了丰富的过滤器和钩子,让您可以自定义编辑体验。以下列举部分功能:
## 编辑器设置
最常见的编辑器修改方式是通过 [`block_editor_settings_all`](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) PHP 过滤器,该过滤器会在设置发送到初始化编辑器之前生效。
`block_editor_settings_all` 钩子会向回调函数传递两个参数:
- `$settings` - 编辑器[可配置设置](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#editor-settings)的数组
- `$context` - [`WP_Block_Editor_Context`](https://developer.wordpress.org/reference/classes/wp_block_editor_context/) 实例,包含当前编辑器信息的对象
以下示例将为无法激活插件的用户(管理员除外)禁用代码编辑器。将此代码添加到插件或主题的 `functions.php` 文件中进行测试:
```php
add_filter( 'block_editor_settings_all', 'example_restrict_code_editor' );
function example_restrict_code_editor( $settings ) {
$can_active_plugins = current_user_can( 'activate_plugins' );
// 对无法激活插件的用户禁用代码编辑器
if ( ! $can_active_plugins ) {
$settings[ 'codeEditingEnabled' ] = false;
}
return $settings;
}
```
更多示例请参阅[编辑器钩子](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/)文档,包含以下用例:
- [设置默认图片尺寸](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#set-a-default-image-size)
- [禁用 Openverse](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#disable-openverse)
- [禁用字体库](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#disable-the-font-library)
- [禁用区块检查器标签页](https://developer.wordpress.org/block-editor/reference-guides/filters/editor-filters/#disable-block-inspector-tabs)
## 服务端 theme.json 过滤器
theme.json 文件是控制界面选项的重要方式,但它仅支持全局或区块层级的修改,在某些场景下可能受限。
例如在前述章节中,我们使用 theme.json 全局禁用了颜色和版式控制。但假设您需要为管理员用户启用颜色设置。
为提供更灵活的配置WordPress 6.1 引入了服务端过滤器,允许在四个数据层自定义 theme.json 数据:
- [`wp_theme_json_data_default`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_default/) - 挂钩 WordPress 提供的默认数据
- [`wp_theme_json_data_blocks`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_blocks/) - 挂钩区块提供的数据
- [`wp_theme_json_data_theme`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) - 挂钩当前主题提供的数据
- [`wp_theme_json_data_user`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_user/) - 挂钩用户提供的数据
以下示例使用 `wp_theme_json_data_theme` 过滤器更新当前主题的 theme.json 数据。如果当前用户是管理员,则会恢复颜色控制功能:
```php
// 为除管理员外的所有用户禁用颜色控制
function example_filter_theme_json_data_theme( $theme_json ){
$is_administrator = current_user_can( 'edit_theme_options' );
if ( $is_administrator ) {
$new_data = array(
'version' => 2,
'settings' => array(
'color' => array(
'background' => true,
'custom' => true,
'customDuotone' => true,
'customGradient' => true,
'defaultGradients' => true,
'defaultPalette' => true,
'text' => true,
),
),
);
}
return $theme_json->update_with( $new_data );
}
add_filter( 'wp_theme_json_data_theme', 'example_filter_theme_json_data_theme' );
```
该过滤器会接收到包含对应数据层数据的 `WP_Theme_JSON_Data` 类实例。随后您需要向 `update_with( $new_data )` 方法传入符合 theme.json 规范的新数据。注意 `$new_data` 中必须包含 theme.json 版本号。

View File

@@ -0,0 +1,90 @@
# 模式
区块[模式](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/)是为用户提供独特定制编辑体验的最佳方式之一。
## 为所有文章类型优先设置起始模式
当用户创建新内容时,无论文章类型如何,都会面对一个空白画布。然而,通过设置特定类型的模式在创建新内容时优先显示,可以改善这一体验。当网站中存在声明支持`core/post-content`区块类型的模式时用户每次创建新项目都会出现模态窗口。默认情况下WordPress不包含任何此类模式因此除非至少添加了两个此类文章内容模式否则模态窗口不会出现。
要启用此功能,请在模式的区块类型中包含`core/post-content`。然后,可以通过文章类型选项控制模式应显示的文章类型。以下是一个在创建新文章时出现的模式示例:
```php
<?php
/**
* 标题:新活动公告
* 别名twentytwentytwo/new-event-announcement
* 区块类型core/post-content
* 文章类型post
* 分类featured, text
*/
?>
<!-- wp:heading {"lock":{"move":false,"remove":true}} -->
<h2>详情</h2>
<!-- /wp:heading -->
<!-- wp:heading {"lock":{"move":false,"remove":true}} -->
<h2>路线指引</h2>
<!-- /wp:heading -->
<!-- wp:heading {"lock":{"move":false,"remove":true}} -->
<h2>预约确认</h2>
<!-- /wp:heading -->
<!-- wp:paragraph {"lock":{"move":true,"remove":true}} -->
<p>如需确认预约请加入Make Slack中的#fse-outreach-experiment频道。</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"lock":{"move":true,"remove":false}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">了解更多</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->
<!-- wp:cover {"useFeaturedImage":true,"dimRatio":80,"overlayColor":"primary","contentPosition":"center center","align":"full"} -->
<div class="wp-block-cover alignfull"><span aria-hidden="true" class="wp-block-cover__background has-primary-background-color has-background-dim-80 has-background-dim"></span><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"输入标题…","fontSize":"large"} -->
<p class="has-text-align-center has-large-font-size">期待您的参与!</p>
<!-- /wp:paragraph --></div></div>
<!-- /wp:cover -->
```
在[WordPress 6.0开发说明中的页面创建模式](https://make.wordpress.org/core/2022/05/03/page-creation-patterns-in-wordpress-6-0/)中了解更多关于此功能的信息,并[注意WordPress 6.1将此功能扩展到所有文章类型](https://make.wordpress.org/core/2022/10/10/miscellaneous-editor-changes-for-wordpress-6-1/#start-content-patterns-for-all-post-types)。
## 为模板创建优先设置起始模式
与为新文章或页面优先设置模式类似,同样的体验可以添加到模板创建过程中。当模式声明支持'templateTypes'属性时只要创建了符合指定条件的模板这些模式就会出现同时提供从空白状态开始或使用当前模板回退选项的选择。默认情况下WordPress不包含任何此类模式。
要启用此功能,模式需要指定一个名为`templateTypes`的属性这是一个包含可使用该模式作为完整内容的模板数组。以下是一个在创建404模板时出现的模式示例
```php
register_block_pattern(
'wp-my-theme/404-template-pattern',
array(
'title' => __( '404专用模板模式', 'wp-my-theme' ),
'templateTypes' => array( '404' ),
'content' => '<!-- wp:paragraph {"align":"center","fontSize":"x-large"} --><p class="has-text-align-center has-x-large-font-size">404模式</p><!-- /wp:paragraph -->',
)
);
```
在[WordPress 6.3开发说明中的新建模板模态窗口模式](https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/#patterns-on-the-create-a-new-template-modal)中了解更多关于此功能的信息。
## 锁定模式
如先前关于锁定API的部分所述模式本身的某些方面可以被锁定以保护设计的重要部分。[这里有一个模式示例](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805),其中包含以不同方式锁定的各种区块。您可以在编辑器中构建这些模式,包括添加锁定选项,然后[按照文档注册它们](/docs/reference-guides/block-api/block-patterns.md)。
## 从模式目录中优先选择特定模式
从WordPress 6.0开始主题可以通过theme.json从[模式目录](https://wordpress.org/patterns/)注册模式。为实现这一点主题应使用theme.json中新的patterns顶级键。在此字段中主题可以列出要从模式目录注册的模式。patterns字段是模式目录中模式别名的数组。模式别名可以从模式目录中单个模式视图的URL中提取。例如此URL https://wordpress.org/patterns/pattern/partner-logos 的别名是partner-logos。
```json
{
"patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ]
}
```
内容创建者随后将在插入器的“模式”选项卡中,找到与模式目录分类匹配的相应模式。
## 附加资源
- [使用模板模式构建多个首页设计](https://developer.wordpress.org/news/2023/04/13/using-template-patterns-to-build-multiple-homepage-designs/)WordPress开发者博客

View File

@@ -0,0 +1,206 @@
# theme.json
主题的 theme.json 文件是定制编辑器体验的最佳方式之一,很可能是在使用更复杂解决方案之前最先用到的工具。
## 提供默认控件/选项
由于 theme.json 作为配置工具,可通过多种方式精细定义可用选项。本节将以双色调为例进行说明,该功能横跨多个区块并支持不同层级的访问权限。
*为所有图片相关区块启用核心双色调选项和自定义功能:*
```json
{
"version": 3,
"settings": {
"color": {
"customDuotone": true,
"duotone": [
]
}
}
}
```
*为所有图片相关区块提供主题预定义色彩选项、核心选项及自定义功能:*
```json
{
"version": 3,
"settings": {
"color": {
"duotone": [
{
"colors": [ "#000000", "#ffffff" ],
"slug": "foreground-and-background",
"name": "前景色与背景色"
},
{
"colors": [ "#000000", "#ff0200" ],
"slug": "foreground-and-secondary",
"name": "前景色与辅助色"
},
{
"colors": [ "#000000", "#7f5dee" ],
"slug": "foreground-and-tertiary",
"name": "前景色与第三色"
},
]
}
}
}
```
*为文章特色图片区块提供预定义默认选项并开放全部自定义功能:*
```json
{
"version": 3,
"settings": {
"color": {
"custom": true,
"customDuotone": true
},
"blocks": {
"core/post-featured-image": {
"color": {
"duotone": [
{
"colors": [ "#282828", "#ff5837" ],
"slug": "black-and-orange",
"name": "黑橙配色"
},
{
"colors": [ "#282828", "#0288d1" ],
"slug": "black-and-blue",
"name": "黑蓝配色"
}
],
"customDuotone": true,
"custom": true
}
}
}
}
}
```
*为文章特色图片区块仅提供预定义默认选项和核心选项(禁用自定义功能):*
```json
{
"version": 3,
"settings": {
"color": {
"custom": true,
"customDuotone": true
},
"blocks": {
"core/post-featured-image": {
"color": {
"duotone": [
{
"colors": [ "#282828", "#ff5837" ],
"slug": "black-and-orange",
"name": "黑橙配色"
},
{
"colors": [ "#282828", "#0288d1" ],
"slug": "black-and-blue",
"name": "黑蓝配色"
}
],
"customDuotone": false,
"custom": false
}
}
}
}
}
```
## 使用 theme.json 限制界面选项
### 按区块限制选项
除了定义默认值,使用 theme.json 还可以完全移除某些选项,转而依赖主题预设配置。下图直观展示了同一段落区块的两种极端设置:
![受限界面示意图](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20comparison%20visual.png?raw=true)
延续双色调的示例,这意味着您可以对图片区块开放全部双色调功能,而仅限制文章特色图片区块的权限:
```json
{
"version": 3,
"settings": {
"color": {
"custom": true,
"customDuotone": true
},
"blocks": {
"core/image": {
"color": {
"duotone": [],
"customDuotone": true,
"custom": true
}
},
"core/post-featured-image": {
"color": {
"duotone": [],
"customDuotone": false,
"custom": false
}
}
}
}
}
```
您可阅读[此文档](/docs/how-to-guides/themes/global-settings-and-styles.md)深入了解如何通过 theme.json 启用/禁用选项。
### 禁用继承默认布局
如需对群组区块等容器区块禁用“继承默认布局”设置,请移除以下配置段:
```json
"layout": {
"contentSize": null,
"wideSize": null
},
```
### 全局限制选项
在区块主题或经典主题中使用 theme.json 时,以下设置将全局禁用默认颜色与排版控件,大幅限制可用功能:
```json
{
"version": 3,
"settings": {
"layout": {
"contentSize": "750px"
},
"color": {
"background": false,
"custom": false,
"customDuotone": false,
"customGradient": false,
"defaultGradients": false,
"defaultPalette": false,
"text": false
},
"typography": {
"customFontSize": false,
"dropCap": false,
"fontStyle": false,
"fontWeight": false,
"letterSpacing": false,
"lineHeight": false,
"textDecoration": false,
"textTransform": false
}
}
}
```
若需启用上述某项功能,只需将对应值改为 `true` 即可实现更精细的控制。

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

View File

@@ -0,0 +1,120 @@
### 区块脚本与样式
在构建区块时,推荐使用 `block.json` 来加载区块本身所需的所有脚本和样式。您可以为编辑器、前端或两者同时加载资源。详见[区块元数据](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/)文档。
### 主题脚本与样式
若需在主题中加载编辑器JavaScript可参照上文使用 `enqueue_block_assets``enqueue_block_editor_assets`。编辑器专用样式表应始终通过 [`add_editor_style()`](https://developer.wordpress.org/reference/functions/add_editor_style/) 或 [`wp_enqueue_block_style()`](https://developer.wordpress.org/reference/functions/wp_enqueue_block_style/) 添加。
`wp_enqueue_block_style()` 函数支持在编辑器及前端加载逐区块样式表。结合 `theme.json` 使用这是样式化区块的最佳方案之一。更多细节请参阅WordPress开发者博客文章[运用theme.json与逐区块样式打造高性能主题](https://developer.wordpress.org/news/2022/12/leveraging-theme-json-and-per-block-styles-for-more-performant-themes/)。
## 向后兼容性与已知问题
通常而言在iframe模式编辑器中加载的资源在WordPress 6.3+版本的非iframe模式下也会同步加载但反向兼容并不总是成立。
若您开发的插件或主题需兼容6.2及以下版本同时保持与WordPress 6.3的兼容性,则无法使用 `enqueue_block_assets` 钩子——因为在6.3之前该钩子不会在iframe编辑器内容中加载资源。
替代方案是使用 `enqueue_block_editor_assets`,只要加载的样式表包含以下任一选择器:`.editor-styles-wrapper``.wp-block``.wp-block-*`。控制台会显示警告信息,但该钩子仍会将样式应用于编辑器内容。
需特别注意从WordPress 6.3开始,为保持向后兼容,通过 `enqueue_block_assets` 加载的资源会在编辑器iframe内外同时加载。根据所加载的脚本库不同这可能引发问题。Gutenberg项目的[GitHub代码库](https://github.com/WordPress/gutenberg/issues/53590)正在持续讨论这一机制。
如果您在使用本指南所述方法时遇到未被报告过的问题请在GitHub上[提交问题报告](https://github.com/WordPress/gutenberg/issues/new/choose)。
# 在编辑器中加载资源
本指南旨在成为在编辑器中加载资源脚本和样式的权威参考。这里概述的方法代表了推荐实践但请注意随着WordPress的发展本资源也会不断更新。欢迎贡献更新内容。
自WordPress 6.3起,如果所有已注册区块都具备[`区块API版本3`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/)或更高版本且未注册传统元框则文章编辑器将采用iframe嵌入。站点编辑器始终采用iframe嵌入。本指南假设您需要为iframe嵌入的编辑器加载资源但请参考下文的后向兼容性章节了解其他注意事项。
若需了解编辑器为何采用iframe嵌入请重温[iframe嵌入模板编辑器中的区块](https://make.wordpress.org/core/2021/06/29/blocks-in-an-iframed-template-editor/)这篇文章。
## 编辑器与编辑器内容
在编辑器中加载资源前,首先需要明确目标范围。
您是要为用户生成内容区块添加样式或JavaScript还是要修改编辑器用户界面组件或与编辑器API交互这可能包括从创建自定义区块控件到注册区块变体等各种场景。
根据这些问题的答案,需要使用不同的钩子。如果您正在构建区块或主题,还需要考虑其他方法。请参考下文对应章节。
## 资源加载场景
### 编辑器脚本与样式
当需要为编辑器本身(即非用户生成内容)加载资源时,应使用[`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/)钩子,配合标准的[`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/)和[`wp_enqueue_style`](https://developer.wordpress.org/reference/functions/wp_enqueue_style/)函数。
典型应用场景包括添加自定义检查器或工具栏控件、通过JavaScript注册区块样式和变体、注册编辑器插件等。
```php
/**
* 加载编辑器资源
*/
function example_enqueue_editor_assets() {
wp_enqueue_script(
'example-editor-scripts',
plugins_url( 'editor-scripts.js', __FILE__ )
);
wp_enqueue_style(
'example-editor-styles',
plugins_url( 'editor-styles.css', __FILE__ )
);
}
add_action( 'enqueue_block_editor_assets', 'example_enqueue_editor_assets' );
```
虽然不推荐,但需注意出于后向兼容考虑,`enqueue_block_editor_assets`也可用于设置编辑器内容样式。详见下文说明。
### 编辑器内容脚本与样式
自WordPress 6.3起,所有通过[`enqueue_block_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_assets/)PHP操作添加的资源都会在iframe嵌入的编辑器中加载。详见[#48286](https://github.com/WordPress/gutenberg/pull/48286)。
这是为用户生成内容区块加载资源的主要方法该钩子会在编辑器环境和网站前端同时触发。不应用于添加针对编辑器UI的资源或与编辑器API交互。后向兼容性说明详见下文。
某些情况下可能只需在编辑器中加载资源而不在前端加载。可通过[`is_admin()`](https://developer.wordpress.org/reference/functions/is_admin/)检测实现。
```php
/**
* 仅限编辑器内加载内容资源
*/
function example_enqueue_editor_content_assets() {
if ( is_admin() ) {
wp_enqueue_script(
'example-editor-content-scripts',
plugins_url( 'content-scripts.js', __FILE__ )
);
wp_enqueue_style(
'example-editor-content-styles',
plugins_url( 'content-styles.css', __FILE__ )
);
}
}
add_action( 'enqueue_block_assets', 'example_enqueue_editor_content_assets' );
```
也可使用[`block_editor_settings_all`](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/)钩子直接修改编辑器设置。此方法实现稍复杂但灵活性更高,仅当`enqueue_block_assets`无法满足需求时使用。
以下示例将所有段落默认文字颜色设为`绿色`
```php
/**
* 通过添加自定义样式修改编辑器设置
*
* @param array $editor_settings 包含当前编辑器设置的数组
* @param string $editor_context 编辑器上下文
*
* @return array 添加自定义CSS样式后的编辑器设置
*/
function example_modify_editor_settings( $editor_settings, $editor_context ) {
$editor_settings["styles"][] = array(
"css" => 'p { color: green }'
);
return $editor_settings;
}
add_filter( 'block_editor_settings_all', 'example_modify_editor_settings', 10,2 );
```
这些样式会以内联方式插入iframe编辑器的`body`中,并添加`.editor-styles-wrapper`前缀。最终生成的标记如下:
```css
<style>.editor-styles-wrapper p { color: green; }</style>
```
从WordPress 6.3开始还可通过这种修改编辑器设置的方法使用JavaScript动态更改样式。详见[#52767](https://github.com/WordPress/gutenberg/pull/52767#top)。

View File

@@ -0,0 +1,107 @@
# 功能标志
「功能标志」是允许您阻止 Gutenberg 项目中的特定代码被发布到 WordPress 核心,并仅在插件中运行某些实验性功能的变量。
## 介绍 `globalThis.IS_GUTENBERG_PLUGIN`
`globalThis.IS_GUTENBERG_PLUGIN` 是一个环境变量,其值用于「标志」代码是否在 Gutenberg 插件内运行。
当代码库为插件构建时,此变量将被设为 `true`。当为 WordPress 核心构建时,它将被设为 `false``undefined`
## 基本用法
### 导出功能
仅限插件使用的函数或常量应使用以下三元语法导出:
```js
function myPluginOnlyFeature() {
// 具体实现
}
export const pluginOnlyFeature = globalThis.IS_GUTENBERG_PLUGIN
? myPluginOnlyFeature
: undefined;
```
在上面的示例中,`pluginOnlyFeature` 导出在非插件环境(例如 WordPress 核心)中将是 `undefined`
### 导入功能
如果您尝试导入并调用仅限插件使用的功能,请务必将函数调用包装在 `if` 语句中以避免错误:
```js
import { pluginOnlyFeature } from '@wordpress/foo';
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
pluginOnlyFeature();
}
```
## 工作原理
在 webpack 构建过程中,`globalThis.IS_GUTENBERG_PLUGIN` 的实例将使用 webpack 的 [define 插件](https://webpack.js.org/plugins/define-plugin/)进行替换。
例如,在以下代码中
```js
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
pluginOnlyFeature();
}
```
变量 `globalThis.IS_GUTENBERG_PLUGIN` 将在仅限插件的构建过程中被替换为布尔值 `true`
```js
if ( true ) {
// Webpack 已将 `globalThis.IS_GUTENBERG_PLUGIN` 替换为 `true`
pluginOnlyFeature();
}
```
这确保了 `if` 语句体内的代码将始终被执行。
在 WordPress 核心中,`globalThis.IS_GUTENBERG_PLUGIN` 变量被替换为 `undefined`。构建后的代码如下所示:
```js
if ( undefined ) {
// Webpack 已将 `globalThis.IS_GUTENBERG_PLUGIN` 替换为 `undefined`
pluginOnlyFeature();
}
```
`undefined` 会被判定为 `false`,因此仅限插件的功能将不会被执行。
### 死代码消除
对于生产构建webpack 会对代码进行[「压缩」](https://en.wikipedia.org/wiki/Minification_(programming)),尽可能移除不必要的 JavaScript。
其中一个步骤涉及称为「死代码消除」的过程。例如当遇到以下代码时webpack 会判定周围的 `if` 语句是不必要的:
```js
if ( true ) {
pluginOnlyFeature();
}
```
该条件将始终判定为 `true`,因此 webpack 会移除它,仅保留原本在体内的代码:
```js
pluginOnlyFeature(); // `if` 条件块已被移除,仅保留体内的代码。
```
类似地,在为 WordPress 核心构建时,以下 `if` 语句中的条件始终解析为 false
```js
if ( undefined ) {
pluginOnlyFeature();
}
```
在这种情况下,压缩过程将移除整个 `if` 语句(包括其体内的代码),确保仅限插件的代码不会包含在 WordPress 核心构建中。
## 常见问题解答
### 为什么我不应该将涉及 `IS_GUTENBERG_PLUGIN` 的表达式结果赋值给变量,例如 `const isMyFeatureActive = ! Object.is( undefined, globalThis.IS_GUTENBERG_PLUGIN )`
引入复杂性可能会阻止 webpack 的压缩器识别并因此消除死代码。因此,建议使用本文档中的示例,以确保您的功能标志按预期工作。更多详细信息,请参阅[死代码消除](#死代码消除)部分。

237
how-to-guides/format-api.md Normal file
View File

@@ -0,0 +1,237 @@
### 步骤4仅对特定区块显示按钮可选
默认情况下,该按钮会显示在每个富文本工具栏中(如图片说明、按钮、段落等)。您可以通过使用[数据API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data),仅针对特定类型的区块显示该按钮。
以下示例仅对段落区块显示按钮:
```js
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';
import { RichTextToolbarButton } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
function ConditionalButton( { isActive, onChange, value } ) {
const selectedBlock = useSelect( ( select ) => {
return select( 'core/block-editor' ).getSelectedBlock();
}, [] );
if ( selectedBlock && selectedBlock.name !== 'core/paragraph' ) {
return null;
}
return (
<RichTextToolbarButton
icon="editor-code"
title="示例输出"
onClick={ () => {
onChange(
toggleFormat( value, {
type: 'my-custom-format/sample-output',
} )
);
} }
isActive={ isActive }
/>
);
}
registerFormatType( 'my-custom-format/sample-output', {
title: '示例输出',
tagName: 'samp',
className: null,
edit: ConditionalButton,
} );
```
### 步骤5在下拉菜单外添加按钮可选
使用 `RichTextToolbarButton` 组件时,按钮会被添加到默认的下拉菜单中。您可以通过使用 `BlockControls` 组件将按钮直接添加到工具栏。
```js
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';
import { BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
const MyCustomButton = ( { isActive, onChange, value } ) => {
return (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
icon="editor-code"
title="示例输出"
onClick={ () => {
onChange(
toggleFormat( value, {
type: 'my-custom-format/sample-output',
} )
);
} }
isActive={ isActive }
/>
</ToolbarGroup>
</BlockControls>
);
};
registerFormatType( 'my-custom-format/sample-output', {
title: '示例输出',
tagName: 'samp',
className: null,
edit: MyCustomButton,
} );
```
## 故障排除
如果遇到错误:
- 请再次确认是否已先运行 `npm run build`
- 确认构建过程中没有语法错误或其他问题。
- 确认JavaScript已在编辑器中加载。
- 检查控制台是否有错误消息。
## 其他资源
本指南中使用的参考文档:
- RichText[`registerFormatType`](/packages/rich-text/README.md#registerformattype)
- 组件:[`RichTextToolbarButton`](/packages/block-editor/README.md#richtexttoolbarbutton)
- RichText[`applyFormat`](/packages/rich-text/README.md#applyformat)
- RichText[`removeFormat`](/packages/rich-text/README.md#removeformat)
- RichText[`toggleFormat`](/packages/rich-text/README.md#toggleformat)
## 结论
本指南向您展示了如何在工具栏中添加按钮,并将其格式应用于选定的文本。请尝试使用它,并在下一个插件中探索更多可能性。
从 [block-development-examples](https://github.com/WordPress/block-development-examples) 仓库下载 [format-api 示例](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86)。
# 格式化工具栏 API
## 概述
格式化 API 使开发者能够向格式化工具栏添加自定义按钮并将特定_格式_应用于文本选区。加粗按钮就是格式化工具栏中标准按钮的一个示例。
![格式化 API 工具栏动态示例](https://developer.wordpress.org/files/2021/12/format-api-example.gif)
在 WordPress 术语中_格式_是指具有[文本级语义的 HTML 标签](https://www.w3.org/TR/html5/textlevel-semantics.html#text-level-semantics-usage-summary),用于为文本选区赋予特殊含义。例如,本教程中要挂钩到格式工具栏的按钮将使用 `<samp>` HTML 标签包裹特定文本选区。
## 开始之前
本指南假设您已熟悉 WordPress 插件及其 JavaScript 加载方式,如需复习请参阅[插件手册](https://developer.wordpress.org/plugins/)或 [JavaScript 教程](/docs/getting-started/fundamentals/javascript-in-the-block-editor.md)。
您需要准备:
- WordPress 开发环境
- 已激活并配置就绪的最小化插件
- 用于构建和入队的 JavaScript 设置
您可参考[完整的 format-api 示例](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86)进行设置。
## 分步指南
本指南将引用 `src/index.js` 作为进行更改的 JavaScript 文件。每步完成后,运行 `npm run build` 会生成 `build/index.js`,该文件随后将加载到文章编辑器界面。
### 步骤 1注册新格式
第一步是注册新格式,在 `src/index.js` 中添加以下内容:
```js
import { registerFormatType } from '@wordpress/rich-text';
registerFormatType( 'my-custom-format/sample-output', {
title: '示例输出',
tagName: 'samp',
className: null,
} );
```
可用格式类型列表由 `core/rich-text` 存储库维护。您可查询存储库以确认自定义格式现已可用。
在浏览器控制台中运行以下代码进行验证:
```js
wp.data.select( 'core/rich-text' ).getFormatTypes();
```
这将返回包含格式类型的数组,其中包括您自定义的格式。
### 步骤 2向工具栏添加按钮
格式就绪后,下一步是通过为 edit 属性注册组件来向 UI 添加按钮。
使用 `RichTextToolbarButton` 组件,更新 `src/index.js`
```js
import { registerFormatType } from '@wordpress/rich-text';
import { RichTextToolbarButton } from '@wordpress/block-editor';
const MyCustomButton = ( props ) => {
return (
<RichTextToolbarButton
icon="editor-code"
title="示例输出"
onClick={ () => {
console.log( '切换格式' );
} }
/>
);
};
registerFormatType( 'my-custom-format/sample-output', {
title: '示例输出',
tagName: 'samp',
className: null,
edit: MyCustomButton,
} );
```
检查功能是否正常:构建并重新加载后,选择任意包含文本的区块(例如段落区块)。确认新按钮已添加到格式工具栏。
![含自定义按钮的工具栏](https://developer.wordpress.org/files/2021/12/format-api-toolbar.png)
点击按钮并在控制台中查看 "toggle format" 消息。
如果未看到按钮或消息,请仔细检查是否正确构建和加载了 JavaScript并查看控制台是否有错误信息。
### 步骤 3点击时应用格式
接下来是更新按钮,在点击时应用格式。
在本示例中,`<samp>` 标签格式是二元的——文本选区要么具有该标签,要么没有,因此我们可以使用 RichText 包中的 `toggleFormat` 方法。
更新 `src/index.js`,修改 `onClick` 操作:
```js
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';
import { RichTextToolbarButton } from '@wordpress/block-editor';
const MyCustomButton = ( { isActive, onChange, value } ) => {
return (
<RichTextToolbarButton
icon="editor-code"
title="示例输出"
onClick={ () => {
onChange(
toggleFormat( value, {
type: 'my-custom-format/sample-output',
} )
);
} }
isActive={ isActive }
/>
);
};
registerFormatType( 'my-custom-format/sample-output', {
title: '示例输出',
tagName: 'samp',
className: null,
edit: MyCustomButton,
} );
```
验证功能:先构建并重新加载,然后选择文本并点击按钮。浏览器可能会以不同于周围文本的方式显示该选区。
您也可以通过切换到 HTML 视图(代码编辑器 `Ctrl+Shift+Alt+M`)来确认,查看被 `<samp>` HTML 标签包裹的文本选区。
注册时使用 `className` 选项可向标签添加自定义类。您可以使用该类配合自定义 CSS 来定位元素并按需设置样式。

View File

@@ -0,0 +1,231 @@
### 测试翻译
您需要将WordPress安装设置为世界语Esperanto语言。请前往“设置”>“常规”,将站点语言更改为世界语。
设置好语言后,创建一篇新文章,添加区块,您将看到所使用的翻译内容。
### 过滤翻译
翻译函数(`__()``_x()``_n()``_nx()`)的输出是可过滤的,完整信息请参阅 [国际化过滤器](/docs/reference-guides/filters/i18n-filters.md)。
# 国际化
## 什么是国际化?
国际化是指为软件提供多语言支持的过程,在本文中特指 WordPress 的国际化。国际化通常缩写为 **i18n**,其中 18 代表首字母 _i_ 和末字母 _n_ 之间的字母数量。
为您的插件和主题提供 i18n 支持能够最大限度地扩大其受众范围,甚至无需您亲自提供额外的语言翻译。当您将软件上传至 WordPress.org 时,所有 JS 和 PHP 文件都将被自动解析。检测到的翻译字符串会被添加到 [translate.wordpress.org](https://translate.wordpress.org/),供社区成员进行翻译,从而确保 WordPress 插件和主题能够支持尽可能多的语言。
对于 PHPWordPress 已建立成熟的流程,请参阅[如何为您的插件进行国际化](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/)。随着 WordPress 5.0 的发布JavaScript 代码的翻译也引入了类似流程。
## 如何在 JavaScript 中使用 i18n
WordPress 5.0 引入了 wp-i18n JavaScript 工具包,它提供了与 PHP 类似的翻译字符串所需功能。
首先,在注册脚本时将 **wp-i18n** 添加为依赖项:
```php
<?php
/**
* 插件名称Myguten 插件
* 文本域myguten
*/
function myguten_block_init() {
wp_register_script(
'myguten-script',
plugins_url( 'block.js', __FILE__ ),
array( 'wp-blocks', 'react', 'wp-i18n', 'wp-block-editor' )
);
register_block_type( 'myguten/simple', array(
'api_version' => 3,
'editor_script' => 'myguten-script',
) );
}
add_action( 'init', 'myguten_block_init' );
```
在代码中,您可以引入 i18n 函数。最常用的函数是 **__**(双下划线),用于简单字符串的翻译。以下是一个基础区块示例:
```js
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'myguten/simple', {
apiVersion: 3,
title: __( '简单区块', 'myguten' ),
category: 'widgets',
edit: () => {
const blockProps = useBlockProps( { style: { color: 'red' } } );
return <p { ...blockProps }>{ __( '你好世界', 'myguten' ) }</p>;
},
save: () => {
const blockProps = useBlockProps.save( { style: { color: 'red' } } );
return <p { ...blockProps }>{ __( '你好世界', 'myguten' ) }</p>;
},
} );
```
在上述示例中,该函数将使用第一个参数作为要翻译的字符串。第二个参数是文本域,必须与插件指定的文本域标识符匹配。
常用函数(这些函数与它们的 PHP 版本相对应)包括:
- `__( '你好世界', 'my-text-domain' )` - 翻译特定字符串。
- `_n( '%s 条评论', '%s 条评论', numberOfComments, 'my-text-domain' )` - 根据给定数字翻译并返回单数或复数形式。
- `_x( '默认', '区块样式', 'my-text-domain' )` - 在特定上下文中翻译字符串。
<div class="callout callout-alert">
<strong>注意:</strong>所有向用户显示的字符串都应使用 i18n 函数进行包装。
</div>
当代码中的所有字符串都完成包装后,最后一步是使用 [wp_set_script_translations()](https://developer.wordpress.org/reference/functions/wp_set_script_translations/) 函数告知 WordPress 您的 JavaScript 包含翻译内容。
```php
<?php
function myguten_set_script_translations() {
wp_set_script_translations( 'myguten-script', 'myguten' );
}
add_action( 'init', 'myguten_set_script_translations' );
```
完成这些步骤即可让您的插件 JavaScript 代码支持翻译。
当您为脚本句柄设置翻译时WordPress 会自动检测 translate.wordpress.org 上是否存在翻译文件。如果存在,则会确保在脚本运行前将其加载到 `wp.i18n` 中。通过 translate.wordpress.org插件开发者无需自行搭建翻译基础设施即可依托拥有数十个活跃语言社区的全球翻译网络。了解更多关于 [WordPress 翻译](https://make.wordpress.org/meta/handbook/documentation/translations/)的信息。
## 提供自定义翻译
若您具备相应语言知识,可通过插件创建并发布专属翻译,确保翻译内容的可用性。
### 创建翻译文件
翻译文件需采用 JED 1.x JSON 格式。
创建 JED 翻译文件时,首先需要从文本中提取字符串。通常语言文件都存放在插件的 `languages` 目录中。通过 [WP-CLI](https://wp-cli.org/) 在插件目录执行以下命令创建 `.pot` 文件:
```bash
mkdir languages
wp i18n make-pot ./ languages/myguten.pot
```
这将生成 `myguten.pot` 文件,其中包含项目中所有可翻译字符串:
```
msgid ""
msgstr ""
"Project-Id-Version: Scratch Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/scratch\n"
"Last-Translator: 全名 <邮箱@地址>\n"
"Language-Team: 语言团队 <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2019-03-08T11:26:56-08:00\n"
"PO-Revision-Date: 年-月-日 时:分+时区\n"
"X-Generator: WP-CLI 2.1.0\n"
"X-Domain: myguten\n"
#. 插件名称
msgid "Scratch Plugin"
msgstr ""
#: block.js:6
msgid "Simple Block"
msgstr ""
#: block.js:13
#: block.js:21
msgid "Hello World"
msgstr ""
```
此处 `msgid` 为待翻译字符串,`msgstr` 为实际翻译内容。在 POT 文件中,`msgstr` 将始终为空。
此 POT 文件可作为新翻译的模板。您需要**复制文件**并使用目标语言代码进行重命名本例使用世界语eo
```bash
cp myguten.pot myguten-eo.po
```
对于简单示例,您可以直接在编辑器中修改 `.po` 文件,为所有 `msgstr` 添加翻译内容。对于更复杂的大型翻译项目,可使用 [GlotPress](https://glotpress.blog/) 和 [Poedit](https://poedit.net/) 工具辅助完成。
同时需要添加 `Language: eo` 参数。以下是完整翻译后的 `myguten-eo.po` 文件:
```
# Copyright (C) 2019
# 本文件遵循与 Scratch 插件相同的许可协议进行分发。
msgid ""
msgstr ""
"Project-Id-Version: Scratch Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/scratch\n"
"Last-Translator: Marcus Kazmierczak <marcus@mkaz.com>\n"
"Language-Team: Esperanto <marcus@mkaz.com>\n"
"Language: eo\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2019-02-18T07:20:46-08:00\n"
"PO-Revision-Date: 2019-02-18 08:16-0800\n"
"X-Generator: Poedit 2.2.1\n"
"X-Domain: myguten\n"
#. 插件名称
msgid "Scratch Plugin"
msgstr "Scratch 扩展插件"
#: block.js:6
msgid "Simple Block"
msgstr "简单区块"
#: block.js:13 block.js:21
msgid "Hello World"
msgstr "世界你好"
```
创建翻译文件的最后一步是将 `myguten-eo.po` 转换为所需的 JSON 格式。可使用 WP-CLI 的 [`wp i18n make-json` 命令](https://developer.wordpress.org/cli/commands/i18n/make-json/)(要求 WP-CLI v2.2.0 及以上版本):
```bash
wp i18n make-json myguten-eo.po --no-purge
```
这将生成包含以下内容的 JSON 文件 `myguten-eo-[md5].json`
```json
{
"translation-revision-date": "2019-04-26T13:30:11-07:00",
"generator": "WP-CLI/2.2.0",
"source": "block.js",
"domain": "messages",
"locale_data": {
"messages": {
"": {
"domain": "messages",
"lang": "eo",
"plural-forms": "nplurals=2; plural=(n != 1);"
},
"Simple Block": [ "简单区块" ],
"Hello World": [ "世界你好" ]
}
}
}
```
### 加载翻译文件
最后一步是告知 WordPress 翻译文件的存放位置。`wp_set_script_translations` 函数支持可选的第三个参数,用于指定优先查找翻译文件的路径。例如:
```php
<?php
function myguten_set_script_translations() {
wp_set_script_translations( 'myguten-script', 'myguten', plugin_dir_path( __FILE__ ) . 'languages' );
}
add_action( 'init', 'myguten_set_script_translations' );
```
WordPress 将按照 `${域名}-${语言代码}-${句柄}.json` 的命名格式在该路径下查找翻译文件。此外,除了使用注册的句柄,也可使用文件相对路径的 md5 哈希值,格式为 `${域名}-${语言代码}-${md5}.json`
使用 `make-json` 命令会自动生成带 md5 哈希值的文件名,可直接使用。您也可以将文件重命名为使用句柄的形式,此时文件名应为 `myguten-eo-myguten-script.json`

265
how-to-guides/metabox.md Normal file
View File

@@ -0,0 +1,265 @@
## 补充资源
- [创建存储文章元数据的自定义区块](https://developer.wordpress.org/news/2023/03/03/creating-a-custom-block-that-stores-post-meta/)
### 第三步:使用文章元数据
您可以通过多种方式使用上一步存储的文章元数据。
#### 在 PHP 中使用文章元数据
第一个示例使用文章元字段的值,并将其包裹在 `H4` 标签中附加到文章内容的末尾。
```php
function myguten_content_filter( $content ) {
$value = get_post_meta( get_the_ID(), 'myguten_meta_block_field', true );
if ( $value ) {
return sprintf( "%s <h4> %s </h4>", $content, esc_html( $value ) );
} else {
return $content;
}
}
add_filter( 'the_content', 'myguten_content_filter' );
```
#### 在区块中使用文章元数据
您也可以在其他区块中使用文章元数据。在此示例中,数据会在每个段落区块渲染时(即显示给用户时)加载到其末尾。您可以根据需要将其替换为任何核心或自定义区块类型。
在 PHP 中,使用 [register_block_type](https://developer.wordpress.org/reference/functions/register_block_type/) 函数设置区块渲染时的回调,以包含元值。
```php
function myguten_render_paragraph( $block_attributes, $content ) {
$value = get_post_meta( get_the_ID(), 'myguten_meta_block_field', true );
// 输出前检查值是否已设置
if ( $value ) {
return sprintf( "%s (%s)", $content, esc_html( $value ) );
} else {
return $content;
}
}
register_block_type( 'core/paragraph', array(
'api_version' => 3,
'render_callback' => 'myguten_render_paragraph',
) );
```
### 第四步:使用区块模板(可选)
使用元区块的一个问题是作者很容易忘记添加,因为它需要添加到每篇文章中。您可以通过使用[区块模板](/docs/reference-guides/block-api/block-templates.md)来解决这个问题。区块模板是按文章类型预定义的区块项列表。模板允许您为文章类型指定默认的初始状态。
在此示例中,您将使用模板自动在文章顶部插入元区块。
将以下代码添加到 `myguten-meta-block.php` 文件中:
```php
function myguten_register_template() {
$post_type_object = get_post_type_object( 'post' );
$post_type_object->template = array(
array( 'myguten/meta-block' ),
);
}
add_action( 'init', 'myguten_register_template' );
```
您还可以在数组中添加其他区块类型,包括占位符,甚至可以将文章锁定为一组特定的区块。模板是控制编辑体验的强大工具,更多信息请参阅上面链接的文档。
## 总结
本指南展示了如何使用区块读取和写入文章元数据。以下部分介绍了与现有元框的向后兼容性。
## 向后兼容性
### 测试、转换和维护现有元框
在将元框转换为区块之前,可以先测试元框是否与区块编辑器兼容,并明确标记。
如果某个元框与区块编辑器不兼容,且无法更新以使其正常工作,下一步是在元框声明中添加 `__block_editor_compatible_meta_box` 参数:
```php
add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback',
null, 'normal', 'high',
array(
'__block_editor_compatible_meta_box' => false,
)
);
```
WordPress 不会显示该元框,而是会显示一条消息,说明它与区块编辑器不兼容,并包含指向经典编辑器插件的链接。默认情况下,`__block_editor_compatible_meta_box``true`
将元框转换为区块后,可以声明其存在以保持向后兼容性:
```php
add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback',
null, 'normal', 'high',
array(
'__back_compat_meta_box' => true,
)
);
```
当使用区块编辑器时,此元框将不再显示在元框区域中,因为它现在仅用于向后兼容。在经典编辑器中,它将像以前一样显示。
### 元数据框数据收集
在每次区块编辑器页面加载时,我们会注册一个用于收集元数据框数据的操作,以判断某个区域是否为空。收集元数据框数据后,原始全局状态将被重置。
详见 [`register_and_do_post_meta_boxes`](https://developer.wordpress.org/reference/functions/register_and_do_post_meta_boxes/)。
该操作将遍历 `post.php` 用于注册元数据框的函数和钩子,包括 `add_meta_boxes``add_meta_boxes_{$post->post_type}``do_meta_boxes`
元数据框会经过过滤,移除所有核心元数据框、标准自定义分类法元数据框,以及任何声明仅用于向后兼容的元数据框。
随后检查该特定类型元数据框的每个位置是否处于活动状态。若非空则存储值为 true若为空则存储值为 false。这些元数据框位置数据随后会通过编辑器 Redux 存储库在 `INITIALIZE_META_BOX_STATE` 中分发。
理想情况下,这可以在编辑器实例化时完成,从而简化流程。但在 `admin_enqueue_scripts`(即调用 `initializeEditor()` 的阶段)之前无法获知元数据框状态。除非我们将 `initializeEditor()` 移至页脚或 `admin_head` 之后的某个阶段执行,否则目前只能如此处理。随着编辑器引导机制的最新改进,现在可能已具备可行性。需使用 ACF 进行测试验证。
### Redux 与 React 元数据框管理
渲染区块编辑器时,元数据框会被渲染到隐藏的 `#metaboxes` 容器中。
*Redux 存储库默认将所有元数据框设为非活动状态*。当接收到 `INITIALIZE_META_BOX_STATE` 时,存储库会将活动元数据框区域的 `isActive` 标志更新为 `true`。随后 React 将检查 Redux 传递给 `MetaBox` 组件的新属性。若该 `MetaBox` 处于活动状态,则会渲染 `MetaBoxArea` 组件而非空内容。`MetaBox` 组件是协调 `MetaBoxArea` 与 Redux 存储库的容器组件。*若无活动元数据框,则保持默认状态。由于所有核心元数据框已被移除,这将成为默认行为。*
#### MetaBoxArea 组件
组件渲染时会存储元数据框容器的引用,并从预取位置获取元数据框的 HTML 内容。
文章更新时,仅会提交处于活动状态的元数据框区域,以避免不必要的请求。元数据框提交不会创建额外修订版本。任何活动元数据框在触发 `REQUEST_POST_UPDATE` 时都会激活 Redux 操作(参见 `editor/effects.js`)。`REQUEST_META_BOX_UPDATES` 操作会将该元数据框状态设为 `isUpdating``isUpdating` 属性将传递至 `MetaBoxArea` 并触发表单提交。
当元数据框区域保存时,我们会显示更新遮罩层,防止用户在保存过程中修改表单值。
示例保存地址形如:
`example.org/wp-admin/post.php?post=1&action=edit&meta-box-loader=1`
该地址通过 `_wpMetaBoxUrl` 全局变量自动传递给 React。
此页面模拟 `post.php` 的文章表单,提交时会触发所有常规钩子和操作,并具备正确的全局状态以正常执行所有 PHP 元数据框逻辑无需修改现有代码。提交成功后React 会触发 `handleMetaBoxReload` 来移除更新遮罩。
### 常见兼容性问题
大多数 PHP 元数据框在区块编辑器中应能继续工作,但包含高级功能的元数据框可能会出现异常。以下是元数据框在区块编辑器中可能无法正常工作的常见原因:
- 依赖针对旧编辑器文章标题、内容字段及其他元数据框选择器的插件
- 依赖 TinyMCE API 的插件(区块编辑器中不再存在单一 TinyMCE 实例)
- 在“提交”或“保存”时更新 DOM 的插件
另请注意:若插件触发输出 PHP 警告或通知,将导致 HTML 文档类型(`<!DOCTYPE html>`)输出异常,从而使浏览器启用“怪异模式”(当浏览器无法识别文档类型时激活的兼容模式)。区块编辑器不适用于此模式,但可能*看似*正常运行。若遇到*元数据框覆盖编辑器*等布局问题请检查网页源代码确认文档类型声明是否为页面首行内容。JavaScript 控制台也会显示相关警告提示。
# 元数据框
## 概述
在区块编辑器出现之前,自定义元数据框被用于扩展编辑器功能。现在有了新的扩展方式,既赋予开发者更多能力,又为内容创作者带来更佳体验。建议将旧版自定义元数据框迁移至以下新方法之一,为编辑器用户创造更统一连贯的体验。
区块编辑器确实支持大多数现有元数据框,详见[下文向后兼容性说明](#向后兼容性)。
若您需要在编辑器外部处理文章元数据,请参阅[侧边栏教程](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md)。
### 使用区块存储元数据
通常区块会将属性值存储在序列化的区块HTML中。但您也可以创建将属性值保存为文章元数据的区块这样就能在模板的任何位置通过编程方式访问这些数据。
本指南将演示如何创建能够提示用户输入单个值,并将其保存为文章元数据的区块。
## 准备工作
本指南假设您已熟悉WordPress插件、文章元数据和基础JavaScript。建议先学习[JavaScript入门教程](/docs/getting-started/fundamentals/javascript-in-the-block-editor.md)打好基础。
虽然本指南将逐步创建基础区块,但推荐通过[创建区块教程](/docs/getting-started/tutorial.md)来更深入理解自定义区块的开发流程。
您需要准备:
- WordPress开发环境
- 已激活并可编辑的基础插件
- 配置好构建和队列加载的JavaScript环境
您可参考[完整的元数据区块示例](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/meta-block-bb1e55)来搭建开发环境。
## 分步指南
1. [注册元字段](#步骤1-注册元字段)
2. [添加元数据区块](#步骤2-添加元数据区块)
3. [使用文章元数据](#步骤3-使用文章元数据)
4. [收尾工作](#步骤4-使用区块模板-可选)
### 步骤1注册元字段
文章元字段是用于存储文章扩展数据的WordPress对象。使用前需先注册新元字段。详见[文章元数据管理](https://developer.wordpress.org/plugins/metadata/managing-post-metadata/)文档。
注册字段时请注意`show_in_rest`参数这能确保数据被包含在REST API中区块编辑器通过该API加载和保存元数据。更多信息请参阅[`register_post_meta`](https://developer.wordpress.org/reference/functions/register_post_meta/)函数说明。
此外,文章类型需支持`custom-fields`才能使`register_post_meta`函数正常工作。
将以下代码添加到PHP插件中以注册字段
```php
<?php
// 注册自定义元标签字段
function myguten_register_post_meta() {
register_post_meta( 'post', 'myguten_meta_block_field', array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
) );
}
add_action( 'init', 'myguten_register_post_meta' );
```
### 步骤2添加元数据区块
完成上一步的元字段注册后,接下来创建用于向用户显示字段值的新区块。
区块可通过`useEntityProp`钩子获取或修改元数据值。
将以下代码添加到JavaScript `src/index.js`
```js
import { registerBlockType } from '@wordpress/blocks';
import { TextControl } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { useBlockProps } from '@wordpress/block-editor';
registerBlockType( 'myguten/meta-block', {
edit: ( { setAttributes, attributes } ) => {
const blockProps = useBlockProps();
const postType = useSelect(
( select ) => select( 'core/editor' ).getCurrentPostType(),
[]
);
const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
const metaFieldValue = meta[ 'myguten_meta_block_field' ];
const updateMetaValue = ( newValue ) => {
setMeta( { ...meta, myguten_meta_block_field: newValue } );
};
return (
<div { ...blockProps }>
<TextControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label="元数据区块字段"
value={ metaFieldValue }
onChange={ updateMetaValue }
/>
</div>
);
},
// 不保存信息至区块
// 数据通过钩子保存至文章元数据
save: () => {
return null;
},
} );
```
创建文章并添加元数据区块即可验证功能。您将看到可输入值的字段。当保存文章(草稿或已发布)时,文章元数据值也会同步保存。可通过保存后重新加载草稿来验证——重新加载后表单仍将保留输入内容。
您还可以检查数据库表`wp_postmeta`确认新文章ID包含新字段数据。
**故障排除**请确保在每次修改后重新构建代码您更新了步骤1的PHP代码且JavaScript文件已正确加入队列。检查构建输出和开发者控制台是否有错误信息。

View File

@@ -0,0 +1,82 @@
# 通知
通知是显示在管理页面顶部附近的信息性用户界面元素。WordPress核心程序、主题和插件都使用通知来指示操作结果或吸引用户关注必要信息。
在经典编辑器中,挂接到`admin_notices`操作的通知可以渲染任意HTML内容。而在区块编辑器中通知则受限于更规范的API。
## 经典编辑器中的通知
在经典编辑器中,"文章草稿已更新"通知的示例如下:
![经典编辑器中的文章草稿更新通知](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/notices/classic-editor-notice.png)
生成等效的"文章草稿已更新"通知需要如下代码:
```php
/**
* 挂接到'admin_notices'操作来渲染
* 通用HTML通知。
*/
function myguten_admin_notice() {
$screen = get_current_screen();
// 仅在文章编辑器中显示此通知。
if ( ! $screen || 'post' !== $screen->base ) {
return;
}
// 渲染通知的HTML内容。
wp_admin_notice(
sprintf( __( '文章草稿已更新。<a href="%s" target="_blank">预览文章</a>' ), get_preview_post_link() ),
array(
'type' => 'success',
'dismissible' => true,
)
);
};
add_action( 'admin_notices', 'myguten_admin_notice' );
```
重要的是,`admin_notices`钩子允许开发者渲染任意HTML内容。这样做的主要优势是开发者拥有极大的灵活性。关键劣势在于任意HTML使得通知功能的后续迭代更加困难甚至不可能因为迭代需要兼容各种任意HTML。这就是区块编辑器采用规范API的原因。
## 区块编辑器中的通知
在区块编辑器中,"文章已发布"通知的示例如下:
![区块编辑器中的文章发布通知](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/notices/block-editor-notice.png)
生成等效的"文章已发布"通知需要如下代码:
```js
( function ( wp ) {
wp.data.dispatch( 'core/notices' ).createNotice(
'success', // 可选类型success, info, warning, error
'文章已发布。', // 显示的文本内容
{
isDismissible: true, // 用户是否可关闭该通知
// 用户可执行的操作
actions: [
{
url: '#',
label: '查看文章',
},
],
}
);
} )( window.wp );
```
当在JavaScript应用程序生命周期中生成通知时您应该使用此_通知数据API_。
为了更好地理解上述具体代码示例:
- `wp`是WordPress的全局窗口变量
- `wp.data`是区块编辑器提供的用于访问数据存储的对象
- `wp.data.dispatch('core/notices')`访问由通知包注册到区块编辑器数据存储的功能
- `createNotice()`是通知包提供的用于注册新通知的函数。区块编辑器通过读取通知数据存储来了解需要显示哪些通知
请查看[_在编辑器中加载资源_](/docs/how-to-guides/enqueueing-assets-in-the-editor.md)教程了解如何将自定义JavaScript加载到区块编辑器中的基础知识。
## 了解更多
区块编辑器提供完整的API用于生成通知。官方文档是了解功能可能性的绝佳资源。
要获取可用操作和选择器的完整列表,请参阅[通知数据手册](/docs/reference-guides/data/data-core-notices.md)页面。

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,60 @@
# 开发平台
古腾堡项目不仅致力于为WordPress打造更优秀的编辑器更在构建一个可扩展的开发平台。该平台包含一系列可用于Web应用程序的JavaScript工具包。[查看npm上的可用工具包列表](https://www.npmjs.com/org/wordpress)。
## 用户界面组件
[WordPress组件包](/packages/components/README.md)提供了一系列可在项目中直接使用的UI组件。访问[WordPress Storybook网站](https://wordpress.github.io/gutenberg/)可交互式查看可用组件及其设置。
以下是在项目中使用组件的快速示例:
安装依赖:
```bash
npm install --save @wordpress/components
```
在React中使用
```jsx
import { Button } from '@wordpress/components';
function MyApp() {
return <Button>Hello Button</Button>;
}
```
多数组件包含样式CSS文件需要引入才能正常显示样式。组件样式表位于`node_modules/@wordpress/components/build-style/style.css`,可直接链接或复制到项目中引入。
## 开发脚本
[`@wordpress/scripts`工具包](/packages/scripts/README.md)是一组可复用的JavaScript开发脚本——包含构建、代码检查和测试脚本无需额外配置文件即可使用。
以下是使用`wp-scripts`工具的快速示例:
安装依赖:
```bash
npm install --save-dev @wordpress/scripts
```
在package.json文件中添加脚本配置
```json
"scripts": {
"build": "wp-scripts build",
"format": "wp-scripts format",
"lint:js": "wp-scripts lint-js",
"start": "wp-scripts start"
}
```
配置后即可使用`npm run build`命令通过预设的webpack配置构建项目代码格式化和检查同理。`start`命令用于开发模式。完整文档请参阅[`@wordpress/scripts`工具包](/packackages/scripts/README.md)。
更多信息请参考区块编辑器手册中的[JavaScript入门教程](/docs/how-to-guides/javascript/js-build-setup.md)。
## 区块编辑器
[`@wordpress/block-editor`工具包](https://developer.wordpress.org/block-editor/packages/packages-block-editor/)支持创建和使用独立的区块编辑器。
详细了解请阅读[“构建自定义区块编辑器”教程](/docs/how-to-guides/platform/custom-block-editor.md)。

View File

@@ -0,0 +1,531 @@
## 总结
恭喜您完成本指南的学习!现在您应该对区块编辑器的工作原理有了更深入的理解。
您刚刚构建的自定义区块编辑器完整代码已[发布于GitHub](https://github.com/getdave/standalone-block-editor),欢迎下载并亲自体验。在实践操作中不断探索,将所学知识推向新的高度。
## 区块持久化
在创建自定义区块编辑器的旅程中,您已经取得了长足进展。但还有一个重要领域有待探讨——区块持久化。换句话说,就是让您的区块在页面刷新之间保持保存并可用。
![展示页面刷新时区块恢复效果的WordPress自定义区块编辑器界面截屏](https://developer.wordpress.org/files/2023/07/custom-block-editor-persistance.gif)
由于这仅是一项实验,本指南选择使用浏览器的`localStorage` API来处理区块数据保存。在实际应用场景中您可能会选择更可靠稳健的系统例如数据库
接下来,让我们深入探讨如何处理区块保存。
### 在状态中存储区块
查看`src/components/block-editor/index.js`文件时,您会注意到已创建了将区块存储为数组的状态:
```jsx
// 文件src/components/block-editor/index.js
const [ blocks, updateBlocks ] = useState( [] );
```
如前所述,`blocks`作为`value`属性传递给"受控"组件`<BlockEditorProvider>`,这为其注入了一组初始区块。同样地,`updateBlocks`设置器被连接到`<BlockEditorProvider>``onInput`回调,确保区块状态与编辑器内对区块所做的更改保持同步。
### 保存区块数据
现在将注意力转向`onChange`处理程序,您会注意到它连接到了一个名为`persistBlocks()`的函数,该函数定义如下:
```js
// 文件src/components/block-editor/index.js
function persistBlocks( newBlocks ) {
updateBlocks( newBlocks );
window.localStorage.setItem( 'getdavesbeBlocks', serialize( newBlocks ) );
}
```
此函数接收已"提交"的区块更改数组,并调用状态设置器`updateBlocks`。同时它还将区块存储在LocalStorage中键名为`getdavesbeBlocks`。为实现这一点,区块数据被序列化为[Gutenberg"区块语法"](https://developer.wordpress.org/block-editor/principles/key-concepts/#blocks)格式,这意味着可以安全地将其存储为字符串。
如果打开开发者工具并检查LocalStorage您会看到序列化的区块数据随着编辑器中发生的更改而存储和更新。以下是该格式的示例
```
<!-- wp:heading -->
<h2>在WordPress后台进行独立区块编辑器的实验</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>本实验旨在探索在WordPress后台创建独立区块编辑器实例的难易程度。</p>
<!-- /wp:paragraph -->
```
### 检索历史区块数据
实现持久化固然重要,但只有当这些数据在每次完整页面重载时被检索并在编辑器中恢复,才能真正发挥作用。
访问数据会产生副作用,因此必须使用`useEffect`钩子来处理:
```jsx
// 文件src/components/block-editor/index.js
useEffect( () => {
const storedBlocks = window.localStorage.getItem( 'getdavesbeBlocks' );
if ( storedBlocks && storedBlocks.length ) {
updateBlocks( () => parse( storedBlocks ) );
createInfoNotice( '区块已加载', {
type: 'snackbar',
isDismissible: true,
} );
}
}, [] );
```
该处理程序:
- 从本地存储中获取序列化的区块数据
- 使用`parse()`工具将序列化区块转换回JavaScript对象
- 调用状态设置器`updateBlocks`,使状态中的`blocks`值更新为从LocalStorage检索到的区块
这些操作的结果是,受控的`<BlockEditorProvider>`组件会使用从LocalStorage恢复的区块进行更新从而使编辑器显示这些区块。
最后,您需要生成一个通知——该通知将以"snackbar"形式显示在`<Notice>`组件中——以指示区块已恢复。
# 构建自定义区块编辑器
WordPress 区块编辑器是一款功能强大的工具,允许您以多种方式创建和格式化内容。其部分功能由 [`@wordpress/block-editor`](/packages/block-editor/README.md) 包驱动,这是一个提供编辑器核心功能的 JavaScript 库。
该软件包还可用于为几乎任何其他网络应用程序创建自定义区块编辑器。这意味着您可以在 WordPress 之外使用相同的区块和区块编辑体验。
![显示内容区块和编辑选项的WordPress区块编辑器界面](https://developer.wordpress.org/files/2023/07/custom-block-editor.png '在自定义WordPress管理页面中运行的独立编辑器实例其中包含示例区块')
这种灵活性和互操作性使区块成为跨多个应用程序构建和管理内容的强大工具。同时也让开发人员能更轻松地创建最适合用户的内容编辑器。
本指南将介绍创建首个自定义区块编辑器的基础知识。
## 引言
尽管古腾堡代码库包含众多软件包和组件,初看可能令人望而生畏。但其核心始终是管理和编辑区块。因此若想在编辑器上进行开发,必须从基础层面理解区块编辑的工作原理。
本指南将引导您在 WordPress 内构建一个功能完整的自定义区块编辑器"实例"。在此过程中,我们将向您介绍关键软件包和组件,帮助您洞悉区块编辑器的工作机制。
阅读完本文后,您将对区块编辑器的内部运作有扎实的理解,并为创建自己的区块编辑器实例奠定坚实基础。
<div class="callout callout-tip">
本指南涉及的完整代码可通过<a href="https://github.com/getdave/standalone-block-editor">配套的WordPress插件</a>下载。该插件中的演示代码是核心学习资源。
</div>
## 代码语法
本指南中的代码片段使用 JSX 语法。当然您也可以选择使用原生 JavaScript。不过许多开发者在熟悉 JSX 后认为其更易读写,因此《区块编辑器手册》中的所有代码示例均采用此语法。
## 即将构建的内容
通过本指南,您将创建一个(近乎)功能完整的区块编辑器实例。最终效果如下所示:
![在自定义WordPress管理页面中运行的独立编辑器实例其中包含示例区块](https://developer.wordpress.org/files/2023/07/custom-block-editor.png)
虽然外观相似但这个编辑器并非您在WordPress中创建文章和页面时熟悉的那个*区块编辑器*,而是一个完全自定义的实例,将存在于名为"区块编辑器"的自定义WordPress管理页面中。
该编辑器将具备以下特性:
- 支持添加和编辑所有核心区块
- 熟悉的视觉样式和主界面/侧边栏布局
- 页面刷新后保持*基础*的区块持久化
## 插件设置与组织架构
自定义编辑器将以WordPress插件形式构建。为简化概念插件将命名为 `Standalone Block Editor Demo`(独立区块编辑器演示),这正是其功能体现。
插件文件结构如下所示:
![包含配置文件和源码的项目目录列表](https://wordpress.org/gutenberg/files/2020/03/repo-files.png 'https://github.com/getdave/standalone-block-editor 插件文件结构截图')
以下是核心文件说明:
- `plugin.php` 包含注释元数据的标准插件入口文件,负责引入 `init.php`
- `init.php` - 处理主插件逻辑的初始化
- `src/` (目录) - 存放 JavaScript 和 CSS 源文件的位置(这些文件*不*由插件直接加载)
- `webpack.config.js` - 自定义 Webpack 配置,扩展了 [`@wordpress/scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) npm 包提供的默认配置以支持自定义CSS样式通过Sass实现
未在上图显示的是 `build/` 目录,该目录用于存放通过 `@wordpress/scripts` 编译输出的 JS 和 CSS 文件。这些文件由插件单独加载。
<div class="callout callout-info">
在本指南中,每个代码片段的顶部都会以注释形式标注文件名引用,方便您对照学习。
</div>
基础文件结构就绪后,接下来让我们了解需要哪些软件包。
### 键盘导航
完成基础组件结构搭建后,最后一步是将所有内容包裹在 [`navigateRegions` 高阶组件](https://github.com/WordPress/gutenberg/tree/e38dbe958c04d8089695eb686d4f5caff2707505/packages/components/src/higher-order/navigate-regions) 中,为布局中的不同"区域"提供键盘导航功能。
```jsx
// 文件src/editor.js
export default navigateRegions( Editor );
```
## 自定义 `<BlockEditor>` 组件
核心布局和组件搭建完成后,现在开始探索区块编辑器本身的自定义实现。
这个组件名为 `<BlockEditor>`,也是实现核心功能的关键所在。
打开 `src/components/block-editor/index.js` 文件可以看到,这是目前遇到的最复杂组件。由于涉及大量逻辑,我们先聚焦于 `<BlockEditor>` 组件渲染的内容:
```js
// 文件src/components/block-editor/index.js
return (
<div className="getdavesbe-block-editor">
<BlockEditorProvider
value={ blocks }
onInput={ updateBlocks }
onChange={ persistBlocks }
settings={ settings }
>
<Sidebar.InspectorFill>
<BlockInspector />
</Sidebar.InspectorFill>
<BlockCanvas height="400px" />
</BlockEditorProvider>
</div>
);
```
其中的关键组件是 `<BlockEditorProvider>``<BlockList>`,下面我们来详细分析。
### 理解 `<BlockEditorProvider>` 组件
[`<BlockEditorProvider>`](https://github.com/WordPress/gutenberg/tree/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/provider) 是组件层级中最重要的组件之一。它为新区块编辑器建立了一个独立的区块编辑上下文环境。
因此,该组件对本项目的核心目标具有*奠基性*意义。
`<BlockEditorProvider>` 的子组件构成了区块编辑器的用户界面。这些组件通过上下文(`Context`)获取数据,从而能够在编辑器内*渲染*和*管理*区块及其行为。
```jsx
// 文件src/components/block-editor/index.js
<BlockEditorProvider
value={ blocks } // 区块对象数组
onInput={ updateBlocks } // 处理区块更新的回调函数
onChange={ persistBlocks } // 处理区块更新/持久化的回调函数
settings={ settings } // 编辑器"设置"对象
/>
```
#### `BlockEditor` 属性说明
可以看到 `<BlockEditorProvider>` 接收一个(已解析的)区块对象数组作为其 `value` 属性。当检测到编辑器内部发生变更时,会调用 `onChange` 和/或 `onInput` 处理函数(将新的区块数组作为参数传递)。
其内部实现机制是通过订阅提供的注册表(通过 [`withRegistryProvider` 高阶组件](https://github.com/WordPress/gutenberg/blob/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/provider/index.js#L158)),监听区块变更事件,判断区块变更是否具有持久性,然后相应调用合适的 `onChange|Input` 处理函数。
在这个简单项目中,这些功能使您可以实现:
- 将当前区块数组作为状态存储于 `blocks`
- 通过调用钩子设置器 `updateBlocks(blocks)`,在 `onInput` 时更新内存中的 `blocks` 状态
- 使用 `onChange` 将区块基础数据持久化到 `localStorage`(该事件在[区块更新被确认为"已提交"时触发](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/provider#onchange)
需要特别注意的是,该组件还接收 `settings` 属性。这里将配置之前在 `init.php` 中以 JSON 格式内联的编辑器设置,可用于配置自定义颜色、可用图片尺寸等功能,以及[更多设置](https://github.com/WordPress/gutenberg/tree/4c472c3443513d070a50ba1e96f3a476861447b3/packages/block-editor#SETTINGS_DEFAULTS)。
## 编辑器的“核心”
虽然 WordPress 编辑器由众多动态组件构成,但其核心是 [`@wordpress/block-editor`](/packages/block-editor/README.md) 包,该包的功能可通过其自述文件精辟概括:
> 此模块允许您创建并使用独立的区块编辑器。
完美!这正是您用来创建自定义区块编辑器实例的核心工具包。但首先,您需要为编辑器创建载体。
## 创建自定义“区块编辑器”页面
让我们从在 WordPress 后台创建自定义页面开始,该页面将承载自定义区块编辑器实例。
<div class="callout callout-info">
如果您已熟悉在 WordPress 中创建自定义后台页面的流程,可以直接<a href="#注册与渲染自定义区块编辑器">跳至后续步骤</a>。
</div>
### 注册页面
您需要使用标准的 WordPress [`add_menu_page()`](https://developer.wordpress.org/reference/functions/add_menu_page/) 辅助函数来[注册自定义后台页面](https://developer.wordpress.org/reference/functions/add_menu_page/)
```php
// 文件init.php
add_menu_page(
'独立区块编辑器', // 页面可见名称
'区块编辑器', // 菜单标签
'edit_posts', // 所需权限
'getdavesbe', // 页面钩子/别名
'getdave_sbe_render_block_editor', // 页面渲染函数
'dashicons-welcome-widgets-menus' // 自定义图标
);
```
`getdave_sbe_render_block_editor` 函数将用于渲染后台页面的内容。温馨提示:每个步骤的源代码均可在[配套插件](https://github.com/getdave/standalone-block-editor)中查看。
### 添加目标 HTML
由于区块编辑器是基于 React 的应用程序,您需要在自定义页面中输出 HTML 容器,以便 JavaScript 渲染区块编辑器。
让我们使用上一步中引用的 `getdave_sbe_render_block_editor` 函数:
```php
// 文件init.php
function getdave_sbe_render_block_editor() {
?>
<div
id="getdave-sbe-block-editor"
class="getdave-sbe-block-editor"
>
编辑器加载中...
</div>
<?php
}
```
该函数会输出基础占位 HTML。请注意 `id` 属性 `getdave-sbe-block-editor`,后续将使用该标识。
### 加载 JavaScript 与 CSS
放置目标 HTML 后,现在可以加载 JavaScript 和 CSS 文件,使其在自定义后台页面生效。
为此,我们需要挂载到 [`admin_enqueue_scripts`](https://developer.wordpress.org/reference/hooks/admin_enqueue_scripts/) 钩子。
首先需确保自定义代码仅运行在目标后台页面。在回调函数开头添加页面标识校验:
```php
// 文件init.php
function getdave_sbe_block_editor_init( $hook ) {
// 非目标页面时退出
if ( 'toplevel_page_getdavesbe' !== $hook ) {
return;
}
}
add_action( 'admin_enqueue_scripts', 'getdave_sbe_block_editor_init' );
```
配置完成后,即可使用 WordPress 标准函数 [`wp_enqueue_script()`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) 安全注册主 JavaScript 文件:
```php
// 文件init.php
wp_enqueue_script( $script_handle, $script_url, $script_asset['dependencies'], $script_asset['version'] );
```
为节省篇幅,此处省略了 `$script_` 变量的赋值过程,您可[在此查看完整代码](https://github.com/getdave/standalone-block-editor/blob/974a59dcbc539a0595e8fa34670e75ec541853ab/init.php#L19)。
注意第三个参数 `$script_asset['dependencies']`,这些依赖项通过 [@wordpress/dependency-extraction-webpack-plugin](https://developer.wordpress.org/block-editor/packages/packages-dependency-extraction-webpack-plugin/) 动态生成,该工具会[确保](https://developer.wordpress.org/block-editor/packages/packages-scripts/#default-webpack-config) WordPress 提供的脚本不会打包到构建文件中。
同时需要注册自定义 CSS 样式和 WordPress 默认格式化库,以应用优美的默认样式:
```php
// 文件init.php
// 加载默认编辑器样式
wp_enqueue_style( 'wp-format-library' );
// 加载自定义样式
wp_enqueue_style(
'getdave-sbe-styles', // 句柄
plugins_url( 'build/index.css', __FILE__ ), // 区块编辑器 CSS
array( 'wp-edit-blocks' ), // 依赖项(确保在该 CSS 之后加载)
filemtime( __DIR__ . '/build/index.css' )
);
```
### 内联编辑器设置
查看 `@wordpress/block-editor` 包时,您会发现它接受一个[设置对象](https://github.com/WordPress/gutenberg/tree/4c472c3443513d070a50ba1e96f3a476861447b3/packages/block-editor#SETTINGS_DEFAULTS)来配置编辑器的默认设置。这些设置在服务器端可用,因此您需要将它们暴露给 JavaScript 使用。
为此,我们将[将设置对象以 JSON 格式内联](https://github.com/getdave/standalone-block-editor/blob/974a59dcbc539a0595e8fa34670e75ec541853ab/init.php#L48),并分配给全局对象 `window.getdaveSbeSettings`
```php
// 文件init.php
// 获取自定义编辑器设置。
$settings = getdave_sbe_get_block_editor_settings();
// 内联所有设置。
wp_add_inline_script( $script_handle, 'window.getdaveSbeSettings = ' . wp_json_encode( $settings ) . ';' );
```
## 注册并渲染自定义块编辑器
通过上述 PHP 代码创建管理页面后,您现在可以使用 JavaScript 将块编辑器渲染到页面的 HTML 中。
首先打开主文件 `src/index.js`,然后引入所需的 JavaScript 包并导入 CSS 样式。请注意,使用 Sass 需要[扩展](https://github.com/getdave/standalone-block-editor/blob/974a59dcbc539a0595e8fa34670e75ec541853ab/webpack.config.js#L13)默认的 `@wordpress/scripts` Webpack 配置。
```js
// 文件src/index.js
// 外部依赖。
import { createRoot } from 'react-dom';
// WordPress 依赖。
import domReady from '@wordpress/dom-ready';
import { registerCoreBlocks } from '@wordpress/block-library';
// 内部依赖。
import Editor from './editor';
import './styles.scss';
```
接下来,一旦 DOM 准备就绪,您需要运行一个函数,该函数将:
-`window.getdaveSbeSettings`(之前从 PHP 内联)获取编辑器设置。
- 使用 `registerCoreBlocks` 注册所有 WordPress 核心块。
-`<Editor>` 组件渲染到自定义管理页面的等待 `<div>` 中。
```jsx
domReady( function () {
const root = createRoot( document.getElementById( 'getdave-sbe-block-editor' ) );
const settings = window.getdaveSbeSettings || {};
registerCoreBlocks();
root.render(
<Editor settings={ settings } />
);
} );
```
<div class="callout callout-info">
无需创建不必要的 JS 全局变量,即可从 PHP 渲染编辑器。请查看 Gutenberg 插件中的 <a href="https://href.li/?https://github.com/WordPress/gutenberg/blob/c6821d7e64a54eb322583a35daedc6c192ece850/lib/edit-site-page.php#L135">编辑站点</a> 包以获取示例。
</div>
## 审查 `<Editor>` 组件
让我们仔细看看上面代码中使用的 `<Editor>` 组件,它位于[配套插件](https://github.com/getdave/standalone-block-editor)的 `src/editor.js` 文件中。
尽管名称如此,但这并不是块编辑器的实际核心。相反,它是一个*包装器*组件,将包含构成自定义编辑器主体的组件。
### 依赖项
`<Editor>` 中的第一件事是引入一些依赖项。
```jsx
// 文件src/editor.js
import Notices from 'components/notices';
import Header from 'components/header';
import Sidebar from 'components/sidebar';
import BlockEditor from 'components/block-editor';
```
其中最重要的是内部组件 `BlockEditor``Sidebar`,稍后将详细介绍。
其余组件主要包括构成编辑器布局和周围用户界面UI的静态元素包括标题和通知区域等。
### 编辑器渲染
有了这些组件后,您可以定义 `<Editor>` 组件。
```jsx
// 文件src/editor.js
function Editor( { settings } ) {
return (
<DropZoneProvider>
<div className="getdavesbe-block-editor-layout">
<Notices />
<Header />
<Sidebar />
<BlockEditor settings={ settings } />
</div>
</DropZoneProvider>
);
}
```
在此过程中,编辑器的核心布局以及一些专门的[上下文提供者](https://react.dev/reference/react/createContext#provider)被搭建起来,这些提供者使特定功能在整个组件层次结构中可用。
让我们更详细地研究这些:
- `<DropZoneProvider>` 启用[拖放功能的放置区域](https://github.com/WordPress/gutenberg/tree/e38dbe958c04d8089695eb686d4f5caff2707505/packages/components/src/drop-zone)
- `<Notices>` 提供一个“消息栏”通知,如果有任何消息发送到 `core/notices` 存储,则会渲染该通知
- `<Header>` 在编辑器 UI 顶部渲染静态标题“独立块编辑器”
- `<BlockEditor>` 自定义块编辑器组件
### 理解 `<BlockList>` 组件
除了 `<BlockEditorProvider>` 之外,下一个最有趣的组件是 [`<BlockList>`](https://github.com/WordPress/gutenberg/blob/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/block-list/index.js)。
这是最重要的组件之一,其作用是**将区块列表渲染到编辑器中**。
它能够实现这一功能,部分原因在于它被放置为 `<BlockEditorProvider>` 的子组件,这使得它可以完全访问编辑器中当前区块状态的所有信息。
#### `BlockList` 是如何工作的?
在底层,`<BlockList>` 依赖于其他几个低级组件来渲染区块列表。
这些组件的层次结构可以*近似*如下:
```jsx
// 以下为伪代码,仅作示例。
<BlockList>
/* 从 rootClientId 渲染区块列表。 */
<BlockListBlock>
/* 从 BlockList 渲染单个区块。 */
<BlockEdit>
/* 渲染区块的标准可编辑区域。 */
<Component /> /* 根据其 `edit()` 实现渲染区块 UI。 */
</BlockEdit>
</BlockListBlock>
</BlockList>
```
以下是这些组件如何协同工作以渲染区块列表的大致过程:
- `<BlockList>` 遍历所有区块的 `clientIds`,并通过 [`<BlockListBlock />`](https://github.com/WordPress/gutenberg/blob/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/block-list/block.js) 渲染每个区块。
- `<BlockListBlock />` 则使用其自身的子组件 [`<BlockEdit>`](https://github.com/WordPress/gutenberg/blob/def076809d25e2ad680beda8b9205ab9dea45a0f/packages/block-editor/src/components/block-edit/index.js) 渲染单个区块。
- 最后,[区块本身](https://github.com/WordPress/gutenberg/blob/def076809d25e2ad680beda8b9205ab9dea45a0f/packages/block-editor/src/components/block-edit/edit.js) 使用 `Component` 占位符组件进行渲染。
`@wordpress/block-editor` 包中的组件是最复杂且涉及最多的组件之一。如果你想从根本上理解编辑器的工作原理,理解这些组件至关重要。强烈建议研究这些组件。
## 审查侧边栏
`<BlockEditor>` 的渲染中,还有一个 `<Sidebar>` 组件。
```jsx
// 文件src/components/block-editor/index.js
return (
<div className="getdavesbe-block-editor">
<BlockEditorProvider>
<Sidebar.InspectorFill> /* <-- 侧边栏 */
<BlockInspector />
</Sidebar.InspectorFill>
<BlockCanvas height="400px" />
</BlockEditorProvider>
</div>
);
```
这部分用于通过 `<BlockInspector>` 组件显示高级区块设置。
```jsx
<Sidebar.InspectorFill>
<BlockInspector />
</Sidebar.InspectorFill>
```
然而,细心的读者可能已经注意到,在 `<Editor>``src/editor.js`)组件的布局中已经存在一个 `<Sidebar>` 组件:
```jsx
// 文件src/editor.js
<Notices />
<Header />
<Sidebar /> // <-- 这是什么?
<BlockEditor settings={ settings } />
```
打开 `src/components/sidebar/index.js` 文件,可以看到这实际上是上面 `<Editor>` 中渲染的组件。然而,该实现使用了 Slot/Fill 来暴露一个 `Fill``<Sidebar.InspectorFill>`),随后该 `Fill` 被导入并在 `<BlockEditor>` 组件中渲染(见上文)。
通过这种方式,你可以将 `<BlockInspector />` 作为 `Sidebar.InspectorFill` 的子组件渲染。这样做的结果是,你可以在 `<BlockEditorProvider>` 的 React 上下文中保留 `<BlockInspector>`,同时允许它在 DOM 中的另一个位置(即 `<Sidebar>` 中)渲染。
这可能看起来过于复杂,但这是为了让 `<BlockInspector>` 能够访问当前区块的信息所必需的。如果没有 Slot/Fill这种设置将极难实现。
至此,你已经了解了自定义 `<BlockEditor>` 的渲染过程。
<div class="callout callout-tip">
<a href="https://github.com/WordPress/gutenberg/blob/def076809d25e2ad680beda8b9205ab9dea45a0f/packages/block-editor/src/components/block-inspector/index.js"><code>&lt;BlockInspector&gt;</code></a>
本身实际上渲染了一个用于 <a href="https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inspector-controls"><code>&lt;InspectorControls&gt;</code></a> 的 <code>Slot</code>。这使你可以<a href="https://github.com/WordPress/gutenberg/blob/def076809d25e2ad680beda8b9205ab9dea45a0f/packages/block-library/src/paragraph/edit.js#L127">在区块的 <code>edit()</code> 定义中渲染</a>一个 <code>&lt;InspectorControls>&gt;</code> 组件,并在编辑器的侧边栏中显示它。建议进一步探索该组件的更多细节。
</div>

View File

@@ -0,0 +1,409 @@
### 步骤三:注册元字段
若要在 `post_meta` 表中操作字段,请使用 [register_post_meta](https://developer.wordpress.org/reference/functions/register_post_meta/) 函数创建名为 `sidebar_plugin_meta_block_field` 的新字段。
注意:此字段需对 REST API 可用,因为区块编辑器通过该接口访问数据。
将以下 PHP 代码添加到插件的 `init` 回调函数中:
```php
register_post_meta( 'post', 'sidebar_plugin_meta_block_field', array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
) );
```
可通过查询区块编辑器存储空间来验证字段是否加载成功。实现后,重新加载编辑器页面并打开浏览器开发者控制台,在控制台中执行以下 JavaScript 代码片段进行确认:
```js
wp.data.select( 'core/editor' ).getCurrentPost().meta;
```
该函数将返回包含已注册元字段的对象。
若代码返回 `undefined`,请确保文章类型支持 `custom-fields`。可通过[注册文章类型](https://developer.wordpress.org/reference/functions/register_post_type/#supports)时设置,或使用 [add_post_type_support 函数](https://developer.wordpress.org/reference/functions/add_post_type_support/)实现。
### 步骤四:初始化输入控件
当字段在编辑器存储中就绪后,即可将其呈现在用户界面中。我们将输入控件提取为独立函数以保持代码整洁,便于后续功能扩展。
```js
( function ( wp ) {
var el = React.createElement;
var registerPlugin = wp.plugins.registerPlugin;
var PluginSidebar = wp.editor.PluginSidebar;
var TextControl = wp.components.TextControl;
var MetaBlockField = function () {
return el( TextControl, {
label: '元区块字段',
value: '初始值',
onChange: function ( content ) {
console.log( '内容已更改为 ', content );
},
} );
};
registerPlugin( 'my-plugin-sidebar', {
render: function () {
return el(
PluginSidebar,
{
name: 'my-plugin-sidebar',
icon: 'admin-post',
title: '我的插件侧边栏',
},
el(
'div',
{ className: 'plugin-sidebar-content' },
el( MetaBlockField )
)
);
},
} );
} )( window.wp );
```
我们需要将 `MetaBlockField` 组件的初始值设为 `sidebar_plugin_meta_block_field` 的值,并在该值变化时保持同步更新。
`useSelect` 函数用于在组件加载时获取数据,并在数据变更时自动更新。以下是使用 `useSelect` 的代码更新版本:
```js
( function ( wp ) {
var el = React.createElement;
var registerPlugin = wp.plugins.registerPlugin;
var PluginSidebar = wp.editor.PluginSidebar;
var Text = wp.components.TextControl;
var useSelect = wp.data.useSelect;
var MetaBlockField = function () {
var metaFieldValue = useSelect( function ( select ) {
return select( 'core/editor' ).getEditedPostAttribute(
'meta'
)[ 'sidebar_plugin_meta_block_field' ];
}, [] );
return el( Text, {
label: '元区块字段',
value: metaFieldValue,
onChange: function ( content ) {
console.log( '内容已更改为 ', content );
},
} );
};
registerPlugin( 'my-plugin-sidebar', {
render: function () {
return el(
PluginSidebar,
{
name: 'my-plugin-sidebar',
icon: 'admin-post',
title: '我的插件侧边栏',
},
el(
'div',
{ className: 'plugin-sidebar-content' },
el( MetaBlockField )
)
);
},
} );
} )( window.wp );
```
`wp.data.useSelect` 函数来自 `@wordpress/data` 包,因此需要在 PHP 的 `wp_register_script` 函数中将 `wp-data` 添加为依赖项。
注意:`getEditedPostAttribute` 调用用于获取文章的最新值,包括用户尚未保存的编辑内容。
更新代码后重新加载并打开侧边栏即可验证功能。此时输入框内容不再是「初始值」而显示为空字符串。虽然用户暂时还无法输入值,但可以验证当存储中的值发生变化时组件是否会更新。打开浏览器控制台执行:
```js
wp.data
.dispatch( 'core/editor' )
.editPost( { meta: { sidebar_plugin_meta_block_field: 'hello world!' } } );
```
即可观察到输入组件中的内容实时更新。
### 步骤5在输入内容变化时更新元字段
最后一步是在输入内容发生变化时更新元字段。
`useDispatch` 函数接收一个存储名称作为唯一参数,并返回可用于更新存储的方法,这里我们将使用 `editPost`
```js
( function ( wp ) {
var el = React.createElement;
var registerPlugin = wp.plugins.registerPlugin;
var PluginSidebar = wp.editor.PluginSidebar;
var TextControl = wp.components.TextControl;
var useSelect = wp.data.useSelect;
var useDispatch = wp.data.useDispatch;
var MetaBlockField = function ( props ) {
var metaFieldValue = useSelect( function ( select ) {
return select( 'core/editor' ).getEditedPostAttribute(
'meta'
)[ 'sidebar_plugin_meta_block_field' ];
}, [] );
var editPost = useDispatch( 'core/editor' ).editPost;
return el( TextControl, {
label: '元区块字段',
value: metaFieldValue,
onChange: function ( content ) {
editPost( {
meta: { sidebar_plugin_meta_block_field: content },
} );
},
} );
};
registerPlugin( 'my-plugin-sidebar', {
render: function () {
return el(
PluginSidebar,
{
name: 'my-plugin-sidebar',
icon: 'admin-post',
title: '我的插件侧边栏',
},
el(
'div',
{ className: 'plugin-sidebar-content' },
el( MetaBlockField )
)
);
},
} );
} )( window.wp );
```
更新后,当用户输入时,输入控件会调用 `editPost` 并在每次按键时更新编辑器存储。
更新 JavaScript 代码,加载侧边栏并在输入框中输入内容。您可以通过在输入控件中输入内容并在浏览器的开发控制台中执行以下 JavaScript 代码片段来确认内容已保存:
```js
wp.data.select( 'core/editor' ).getEditedPostAttribute( 'meta' )[
'sidebar_plugin_meta_block_field'
];
```
显示的消息应该是您在输入框中输入的内容。
在保存文章时,您可以通过保存后重新加载页面并确认输入控件已初始化为您最后输入的值,来验证内容是否正确存储到数据库中。
## 附加资源
有关使用 [@wordpress/data 包](/packages/data/README.md) 的文档。
本指南中使用的函数:
- [useSelect](/packages/data/README.md#useselect)
- [getEditedPostAttribute](/docs/reference-guides/data/data-core-editor.md#geteditedpostattribute)
- [useDispatch](/packages/data/README.md#usedispatch)
## 总结
您现在拥有一个自定义侧边栏,可用于更新 `post_meta` 内容。
完整示例可在 [block-development-examples](https://github.com/WordPress/block-development-examples) 代码库中下载 [plugin-sidebar 示例](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/plugin-sidebar-9ee4a6)。
### 注意
如果您在编辑器“偏好设置”的“面板”页面中启用了“自定义字段”(通过右上角的三个点),与 TextControl 同名的字段(在本例中为 `sidebar_plugin_meta_block_field`)也会出现在编辑器窗口底部的自定义字段面板中。这两个字段可以访问相同的元属性。
![文本控件与自定义字段](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/plugin-sidebar-text-control-custom-field.png)
在保存文章时TextControl 中的值会先保存,而自定义字段中的值会后保存,因此最终保存到数据库的是自定义字段中的值。因此,如果您更改了 TextControl 中的值,最终保存的仍然是自定义字段中的值。
如果未启用自定义字段,则不会出现此问题。
如果您需要启用自定义字段并在侧边栏中显示文章元数据,有两种可能的解决方案:
1. 在元字段名称前添加下划线,例如上述示例中的名称改为 `_sidebar_plugin_meta_block_field`。这表示该文章元数据应视为私有,不会在文章的自定义字段部分显示。使用此解决方案时,除非您在传递给 `register_post_meta``args` 数组中添加一个 `auth_callback` 属性,并提供一个最终返回 `true` 的函数,否则在保存文章时会生成错误。有关更多信息,请参阅 [post_meta](https://developer.wordpress.org/reference/functions/register_meta/#parameters) 页面中的 `args` 文档。
2. 在 TextControl 的 `onChange` 函数中,将目标指向值字段的文本区域,并将其值设置为与 TextControl 元字段中的值相同。这样,两个位置的值将保持一致,因此您可以确保即使更改了 TextControl 中的值,它仍然会被保存到数据库中。
```js
return el( TextControl, {
label: '元区块字段',
value: metaFieldValue,
onChange: function ( content ) {
editPost( {
meta: { sidebar_plugin_meta_block_field: content }
})
document.querySelector( {值字段文本区域} ).innerHTML = content;
},
} );
```
# 插件侧边栏
## 概述
如何为插件添加侧边栏。侧边栏位于编辑器最右侧区域。您的插件可以在检查器控件(齿轮图标)旁添加额外图标,该图标可展开显示。
![侧边栏示例](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/sidebar-up-and-running.png)
*注意:本教程主要讲解自定义侧边栏,如需在侧边栏添加控件请参阅[区块工具栏与设置侧边栏](/docs/getting-started/fundamentals/block-in-the-editor.md)*
## 准备工作
本教程假设您已具备插件基础环境并准备添加PHP和JavaScript代码。请先参阅[JavaScript入门指南](/docs/getting-started/fundamentals/javascript-in-the-block-editor.md)了解WordPress插件基础知识及如何使用JavaScript扩展区块编辑器。
## 分步指南
### 步骤1启动侧边栏
首先需要告知编辑器存在包含独立侧边栏的新插件。请分别使用`@wordpress/plugins``@wordpress/editor``react`包提供的[registerPlugin](/packages/plugins/README.md)、[PluginSidebar](/packages/editor/README.md#pluginsidebar)和[createElement](/packages/element/README.md)工具。
将以下代码添加至名为`plugin-sidebar.js`的JavaScript文件并保存到插件目录
```js
( function ( wp, React ) {
var el = React.createElement;
var registerPlugin = wp.plugins.registerPlugin;
var PluginSidebar = wp.editor.PluginSidebar;
registerPlugin( 'my-plugin-sidebar', {
render: function () {
return el(
PluginSidebar,
{
name: 'my-plugin-sidebar',
icon: 'admin-post',
title: '我的插件侧边栏',
},
'元字段'
);
},
} );
} )( window.wp, window.React );
```
为使代码正常运行,需确保这些工具在浏览器中可用,因此必须将`wp-plugins``wp-editor``react`指定为脚本依赖项。
以下是注册脚本并声明依赖项的PHP代码
```php
<?php
/*
插件名称:侧边栏插件
*/
function sidebar_plugin_register() {
wp_register_script(
'plugin-sidebar-js',
plugins_url( 'plugin-sidebar.js', __FILE__ ),
array( 'wp-plugins', 'wp-editor', 'react' )
);
}
add_action( 'init', 'sidebar_plugin_register' );
function sidebar_plugin_script_enqueue() {
wp_enqueue_script( 'plugin-sidebar-js' );
}
add_action( 'enqueue_block_editor_assets', 'sidebar_plugin_script_enqueue' );
```
安装并启用此插件后,编辑器右上角将出现新的图钉图标。点击即可展开插件侧边栏:
![运行中的侧边栏](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/sidebar-up-and-running.png)
### 步骤2调整样式并添加控件
侧边栏运行后,接下来需要填充必要组件和基础样式。
为可视化编辑元字段值,将使用输入组件。`@wordpress/components`包提供多种可复用组件,其中[TextControl](/packages/components/src/text-control/README.md)专用于创建输入字段:
```js
( function ( wp ) {
var el = React.createElement;
var registerPlugin = wp.plugins.registerPlugin;
var PluginSidebar = wp.editor.PluginSidebar;
var TextControl = wp.components.TextControl;
registerPlugin( 'my-plugin-sidebar', {
render: function () {
return el(
PluginSidebar,
{
name: 'my-plugin-sidebar',
icon: 'admin-post',
title: '我的插件侧边栏',
},
el(
'div',
{ className: 'plugin-sidebar-content' },
el( TextControl, {
label: '元区块字段',
value: '初始值',
onChange: function ( content ) {
console.log( '内容已更改为 ', content );
},
} )
)
);
},
} );
} )( window.wp );
```
使用新代码更新`plugin-sidebar.js`。注意代码使用了`@wordpress/components`包中的`wp.components`工具请确保在PHP文件的`wp_register_script`函数中添加`wp-components`依赖项。
代码新增内容:
-`div`元素添加CSS类`plugin-sidebar-content`用于样式定位
- 使用`TextControl`组件替代纯文本'元字段'
通过新增的CSS类可添加基础样式。在插件目录创建`plugin-sidebar.css`文件并添加内边距设置:
```css
.plugin-sidebar-content {
padding: 16px;
}
```
注册样式表并通过`enqueue_block_editor_assets`与JavaScript文件同时加载。
完成修改后PHP代码如下所示
```php
<?php
/*
插件名称:侧边栏示例
*/
function sidebar_plugin_register() {
wp_register_script(
'plugin-sidebar-js',
plugins_url( 'plugin-sidebar.js', __FILE__ ),
array(
'react',
'wp-plugins',
'wp-editor',
'wp-components'
)
);
wp_register_style(
'plugin-sidebar-css',
plugins_url( 'plugin-sidebar.css', __FILE__ )
);
}
add_action( 'init', 'sidebar_plugin_register' );
function sidebar_plugin_script_enqueue() {
wp_enqueue_script( 'plugin-sidebar-js' );
wp_enqueue_style( 'plugin-sidebar-css' );
}
add_action( 'enqueue_block_editor_assets', 'sidebar_plugin_script_enqueue' );
```
重新加载编辑器并打开侧边栏:
![含样式和控件的侧边栏](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/sidebar-style-and-controls.png)
当前代码尚未实现数据存储与检索功能,后续步骤将重点介绍如何连接元区块字段。

View File

@@ -0,0 +1,60 @@
# 区块类型的更新传播
本文旨在为需要更新内容的用户提供指导,无论是更新主题模板、模式还是整个网站的区块。由于每种内容类型允许或禁止特定类型的同步,在创建前了解可能性的范围对简化未来维护至关重要。
## 更新管理建议
### 预先明确需要更新的内容
从宏观角度来看,需认识到并非所有内容都能在全站范围内更新,且创建方式会极大影响可操作性。因此,在创建前花时间确定需要更新的内容并将其置于合适的格式中至关重要。这将显著影响未来的维护效率。
### 拥抱区块级主题设计
区块主题设计需要从传统设计思维转变——不再局限于设计大版块并通过更新控制。虽然整体设计视角在创建自定义主题项目时仍然重要,但区块要求设计师以更原子化的方式进行设计。这意味着从区块本身出发,通常通过 theme.json 自定义实现。**核心目标是让每个独立“原子”(即区块)可自由移动、编辑、删除和重组,而不会破坏整体设计。**
越倾向于区块级设计,就越不需要在全站范围内向模式和模板等元素传播更新。只要原子化部件到位,其布局不应成为问题。
## 内容类型(及其正确更新方式)
### 区块
区块更新的管理方式取决于区块本身的性质。若区块依赖外部数据,建议从一开始就通过 `render_callback` 函数将其设为动态区块,这样能提供更多控制权。若区块结构预计会随时间变化,则推荐从使用 `save()` 方法定义默认输出的静态区块开始。后续可逐步采用混合方案,加入能使用 `save` 输出作为备选的 `render_callback` 以处理替代输出。需注意,这种灵活性和控制力是以渲染时的额外处理为代价的。另一种方案是使用依赖[区块弃用机制](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/)管理更新的静态区块,但这需要手动更新现有区块。根据需求和使用习惯,两种方案皆可适用。**若要快速开始创建区块,[可使用 Create Block 工具](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/)。**
### 模式
**对于后续需要更新的内容,请勿使用模式,应改用可重用区块或模板部件。** 模式插入站点后无法更新。可以将其理解为示例/样板/初始内容。虽然插入器中展示的模式会随时间演进,但这些变更不会自动应用到当前使用的模式实例上。一旦插入,模式就会与原始模式完全脱离(这点与可重用区块或模板部件区块不同)。
如需为自定义样式的模式设置解决方案,可考虑在包装区块中添加类名。例如以下代码为群组区块添加 themeslug-special 类:
```
<!-- wp:group {"className":"themeslug-special"} -->
<div class="wp-block-group themeslug-special">
<!-- 嵌套的模式区块 -->
</div>
<!-- /wp:group -->
```
这种方法并非万无一失用户仍可通过编辑器界面修改类名。但由于该设置位于“高级”面板下大多数情况下会保持原样。这为主题开发者提供了一定程度的CSS控制权使其能更新现有使用实例但无法阻止用户进行无法更新的重大修改。
### 同步模式
顾名思义这类模式天然支持全站同步。需注意依赖同步模式处理特定更新目前存在局限——更新时内容、HTML结构和样式将保持同步。若需要更精细的控制这是需要考量的关键因素此时动态区块可能是更优选择。
### 模板部件与模板
由于区块主题允许用户直接编辑模板和模板部件,管理变更的方式需根据用户更高权限进行调整。具体来说,当用户修改模板或模板部件后,主题更新提供的新模板将不会对已修改用户显示。只有新主题用户或未编辑过模板的用户会体验到更新后的模板。若用户未修改文件,您在文件系统中的变更将直接体现在用户站点上——只需更新文件即可生效。但若用户已修改模板,则更新其模板的唯一方式是:
- 回滚所有用户修改
- 更新数据库中的模板和模板部件
一般而言,若用户已修改模板,建议维持现状(除非在代理场景中与用户达成共识)。
更新模板时需注意对新增或不同模板部件的引用。例如 page.html 模板在1.0版本中引用 parts/header.html而在2.0版本中改为引用 parts/header-alt.html。某些开发者可能将此视为用户修改原 header.html 时的“解决方案”,但这很可能破坏用户的定制设计,因为 page.html 模板将不再引用正确部件(除非用户也修改并保存了页面模板)。
同样地,在主题更新中删除模板部件通常不可取。这种情况下,用户可能创建了引用该部件的自定义顶层模板,并预期其持续存在。
## 相关资源
- [模式、模板部件与可重用区块对比](https://wordpress.org/documentation/article/comparing-patterns-template-parts-and-reusable-blocks/)
- [区块弃用机制](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/)
- [Create Block 工具](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/)

View File

@@ -0,0 +1,22 @@
# 主题
区块编辑器为主题设计师和开发者提供了多种交互选项,包括主题定义的颜色设置、字体大小控制等功能。
## 主题类型
### 经典主题
在区块编辑器的术语体系中,指那些采用传统`.php`文件格式定义模板,且在`/block-templates``/templates`文件夹中不包含`index.html`格式模板的主题。经典主题可通过[主题支持功能](/docs/how-to-guides/themes/theme-support.md)或引入[theme.json](/docs/how-to-guides/themes/global-settings-and-styles.md)文件,为区块编辑器及区块内容提供配置与样式选项。即使不是区块主题,也能通过使用`theme.json`文件获得灵活配置能力。
### 区块主题
指至少在`/block-templates``/templates`文件夹中包含`index.html`格式模板,并以区块内容标记形式提供模板的主题。虽然多数区块主题会使用`theme.json`文件进行配置和样式设置,但该文件并非区块主题的必备要素。区块主题的优势在于可以通过区块编辑器编辑网站所有区域:页眉、页脚、侧边栏等。
### 全站编辑FSE
并不存在特定的FSE主题类型。在WordPress 5.9及以上版本中,所有区块主题(即在`/block-templates``/templates`文件夹中包含`index.html`格式模板的主题)均支持全站编辑功能。
**目录**
- [全局设置theme.json](/docs/how-to-guides/themes/global-settings-and-styles.md)
- [主题支持](/docs/how-to-guides/themes/theme-support.md)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,511 @@
## 链接颜色控制
链接颜色支持已在 WordPress 5.8 版本中趋于稳定。该功能默认处于关闭状态(`false`),主题可通过 [theme.json 文件](/docs/how-to-guides/curating-the-editor-experience/theme-json.md)启用:
```json
{
"settings": {
"color": {
"link": true
}
}
}
```
> 替代方案:若已启用 Gutenberg 插件,传统的实验性支持 `add_theme_support( 'experimental-link-color' )` 仍可生效。当 Gutenberg 插件要求最低 WordPress 版本为 5.9 时,此备用方案将被移除。
当用户设置区块的链接颜色时,系统将以如下形式添加新样式:
```css
.wp-elements-<uuid> a {
color: <link-color> !important;
}
```
其中:
- `<uuid>` 为随机生成的数字
- `<link-color>` 可以是 `var(--wp--preset--color--slug)`(当用户选择预设值时)或原始颜色值(当用户选择自定义值时)
区块将被附加 `.wp-elements-<uuid>` 类名。
## 外观工具
通过此设置启用以下全局样式配置项:
- 边框:颜色、圆角、样式、宽度
- 颜色:链接色彩
- 间距:区块间隙、外边距、内边距
- 排版:行高
- 尺寸:宽高比、最小高度
- 定位:粘性定位
```php
add_theme_support( 'appearance-tools' );
```
## 边框
启用完整边框设置功能:
```php
add_theme_support( 'border' );
```
## 链接颜色
启用链接颜色设置功能:
```php
add_theme_support( 'link-color' );
```
## 区块化模板部件
区块化模板部件允许管理员使用区块编辑网站局部组件。此功能默认关闭,需要主题通过声明支持来启用:
```php
add_theme_support( 'block-template-parts' );
```
该功能仅对非区块主题有实际意义,因为区块主题已通过站点编辑器原生支持区块化模板部件。
独立模板部件编辑器不允许编辑者创建新模板部件或删除现有模板部件,这是因为主题需要手动在 PHP 模板中包含模板部件。
您可在[主题手册的区块模板与模板部件章节](https://developer.wordpress.org/themes/block-themes/templates-and-template-parts/#block-c5fa39a2-a27d-4bd2-98d0-dc6249a0801a)中了解更多关于区块化模板部件的信息。
# 主题支持
新版区块功能为所有主题提供了基础支持,同时包含可选的增强功能以及扩展和自定义能力。
构建主题时需要考虑以下几个新概念:
- **编辑器调色板** - 系统提供默认颜色集,但主题可以注册自己的颜色,并可选地将用户选择限制在预设调色板范围内。
- **编辑器字号面板** - 系统提供默认字号集,但主题可以注册自己的字号,并可选地将用户选择限制在预设字号范围内。
- **响应式嵌入内容** - 主题需主动启用响应式嵌入功能。
- **前端与编辑器样式** - 为充分发挥区块功能,主题开发者需确保核心样式正常显示并启用,或编写与自身主题最适配的自定义样式。
- **区块工具** - 主题可选择启用多种区块工具,如行高设置、自定义单位等。
- **核心区块模式** - 主题可选择禁用默认区块模式。
默认情况下,区块会提供自身样式以确保在未修改的主题中正常显示。同时它们还[提供可选的预设样式](#默认区块样式)。主题可以添加/覆盖这些样式,也可以完全不提供样式,完全依赖区块自带的样式。
某些高级区块功能需要主题本身启用支持,因为这些样式难以由区块直接提供,可能需要主题本身进行架构层面的调整才能完美适配。
要启用这些功能,请在主题的 `functions.php` 文件中调用 `add_theme_support`。例如:
```php
function mytheme_setup_theme_supported_features() {
add_theme_support( 'editor-color-palette', array(
array(
'name' => esc_attr__( '强品红色', 'themeLangDomain' ),
'slug' => 'strong-magenta',
'color' => '#a156b4',
),
array(
'name' => esc_attr__( '浅灰品红色', 'themeLangDomain' ),
'slug' => 'light-grayish-magenta',
'color' => '#d0a5db',
),
array(
'name' => esc_attr__( '极浅灰色', 'themeLangDomain' ),
'slug' => 'very-light-gray',
'color' => '#eee',
),
array(
'name' => esc_attr__( '深灰色', 'themeLangDomain' ),
'slug' => 'very-dark-gray',
'color' => '#444',
),
) );
}
add_action( 'after_setup_theme', 'mytheme_setup_theme_supported_features' );
```
## 可选功能
## 默认区块样式
核心区块包含默认的结构样式。这些样式默认会在编辑器和前端同时加载。例如控制分栏区块的CSS规则若缺少这些规则该区块将呈现破碎布局且无法显示分栏效果。
### 预设区块样式
区块编辑器允许主题为前端启用更具设计主张的样式。例如引用区块左侧的默认彩色条。若要在经典主题中使用这些预设样式,请添加对 `wp-block-styles` 的主题支持:
```php
add_theme_support( 'wp-block-styles' );
```
您可以在[区块库主题文件](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/theme.scss)中查看将被引入的CSS代码。
对于区块主题或提供 `theme.json` 文件的主题,不建议使用此主题支持功能。为确保全局样式规则与区块样式之间不存在样式冲突,请将所需的区块样式添加到主题的 `theme.json` 文件中。
### 宽对齐功能:
某些区块(如图像区块)可通过向区块包装器添加对应类名(`alignwide``alignfull`)来定义“宽”或“全宽”对齐方式。主题可通过以下调用启用此功能:
```php
add_theme_support( 'align-wide' );
```
有关此功能的更多信息,请参阅[关于 `add_theme_support()` 的开发文档](https://developer.wordpress.org/reference/functions/add_theme_support/)。
### 支持自定义单位
除了像素单位外用户还可使用其他单位定义尺寸、内边距等。可用单位包括px、em、rem、vh、vw。主题可通过以下代码禁用此功能
```php
add_theme_support( 'custom-units', array() );
```
主题还可对可用自定义单位进行筛选。
```php
add_theme_support( 'custom-units', 'rem', 'em' );
```
### 禁用默认区块模式
WordPress 内置了多种区块模式,主题可通过以下代码选择不启用捆绑模式,转而提供自有模式集:
```php
remove_theme_support( 'core-block-patterns' );
```
## 编辑器样式
区块编辑器支持主题的[编辑器样式](https://codex.wordpress.org/Editor_Style),但其运作方式与经典编辑器略有不同。
在经典编辑器中编辑器样式表会直接加载到所见即所得编辑器的iframe内不做任何更改。而区块编辑器并不使用iframe。为确保样式仅应用于编辑器内容系统会通过选择性重写或调整特定CSS选择器来自动转换编辑器样式。这也使得区块编辑器可以在区块变体预览中利用您的编辑器样式。
例如,如果您在编辑器样式中编写`body { ... }`,这将被重写为`.editor-styles-wrapper { ... }`。这也意味着您不应直接针对任何编辑器类名进行样式设定。
由于其运作方式不同,您需要在`add_editor_style`函数之外,额外在主题中添加以下代码片段来启用此功能:
```php
add_theme_support( 'editor-styles' );
```
您无需过多调整编辑器样式;大多数主题只需添加上述代码片段,即可在经典编辑器和区块编辑器中获得相似效果。
### 载入编辑器样式
使用`add_editor_style`函数在编辑器界面载入CSS样式。对于经典编辑器这是为编辑器添加样式的唯一必要函数。对于新版区块编辑器您需要先添加前述的`add_theme_support( 'editor-styles');`
```php
add_editor_style( 'style-editor.css' );
```
将此代码添加到`functions.php`文件后,会将`style-editor.css`样式表加入编辑器待加载样式表队列。
### 基础色彩
您可以像设置普通网页那样设置编辑器样式。以下是将背景色和字体颜色改为蓝色的方法:
```css
/* 将此代码添加至`style-editor.css`文件 */
body {
background-color: #d3ebf3;
color: #00005d;
}
```
### 调整编辑器宽度
要更改编辑器的主列宽请将以下CSS添加至`style-editor.css`
```css
/* 主列宽度 */
.wp-block {
max-width: 720px;
}
/* "宽版"区块宽度 */
.wp-block[data-align='wide'] {
max-width: 1080px;
}
/* "全宽"区块宽度 */
.wp-block[data-align='full'] {
max-width: none;
}
```
您可以使用这些编辑器宽度值来匹配主题设置。可使用任何CSS宽度单位包括`%``px`
扩展阅读:[使用样式表应用样式](/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md)。
## 响应式嵌入内容
嵌入区块会自动为嵌入内容应用样式以反映在iFrame中嵌入内容的长宽比。应用长宽比响应式样式的区块显示效果如下
```html
<figure class="wp-embed-aspect-16-9 wp-has-aspect-ratio">...</figure>
```
为使内容在保持长宽比的同时自适应尺寸,`<body>`元素需要具有`wp-embed-responsive`类。该样式类默认未设置,需要主题通过声明支持`responsive-embeds`功能来启用:
```php
add_theme_support( 'responsive-embeds' );
```
## 间距控制
部分区块可配备内边距控制功能。此功能默认关闭,需要主题通过声明支持来启用:
```php
add_theme_support( 'custom-spacing' );
```
### 区块字体大小
区块可能允许用户配置所使用的字体大小,例如段落区块。区块提供一组默认字体大小,但主题可以覆盖并提供自己的设置:
```php
add_theme_support( 'editor-font-sizes', array(
array(
'name' => esc_attr__( '小号', 'themeLangDomain' ),
'size' => 12,
'slug' => 'small'
),
array(
'name' => esc_attr__( '常规', 'themeLangDomain' ),
'size' => 16,
'slug' => 'regular'
),
array(
'name' => esc_attr__( '大号', 'themeLangDomain' ),
'size' => 36,
'slug' => 'large'
),
array(
'name' => esc_attr__( '超大号', 'themeLangDomain' ),
'size' => 50,
'slug' => 'huge'
)
) );
```
字体大小将按照主题提供的顺序显示在字体大小选择器中。
主题负责创建应用正确字体大小样式的类。类名通过添加 'has-',后接使用短横线命名法的字体大小名称,并以 `-font-size` 结尾来构建。
以常规字体大小为例,主题可提供以下类:
```css
.has-regular-font-size {
font-size: 16px;
}
```
<div class="callout callout-info">
<strong>注意:</strong>标识符 `default``custom` 为保留字,主题不可使用。
</div>
自 WordPress 5.9 起,要覆盖核心定义的字体大小值,没有 `theme.json` 的主题必须通过 CSS 自定义属性设置其值而非提供类。CSS 自定义属性使用以下命名方式 `--wp--preset--font-size--<slug>`。更多信息请参阅[此开发说明](https://make.wordpress.org/core/2022/01/08/updates-for-settings-styles-and-theme-json/)。例如:
```css
:root {
--wp--preset--font-size--small: <>;
--wp--preset--font-size--large: <>;
}
```
### 禁用自定义字体大小
主题可通过以下代码禁用设置自定义字体大小的功能:
```php
add_theme_support( 'disable-custom-font-sizes' );
```
设置后,用户将被限制使用区块编辑器提供的默认大小或通过 `editor-font-sizes` 主题支持设置提供的大小。
### 禁用区块调色板中的自定义颜色
默认情况下,提供给区块的调色板允许用户选择与编辑器或主题默认颜色不同的自定义颜色。
主题可通过以下方式禁用此功能:
```php
add_theme_support( 'disable-custom-colors' );
```
此标志将确保用户只能从主题提供的 `editor-color-palette` 中选择颜色,或者如果主题未提供,则从编辑器的默认颜色中选择。
### 禁用自定义渐变
主题可通过以下代码禁用设置自定义渐变的功能:
```php
add_theme_support( 'disable-custom-gradients' );
```
设置后,用户将被限制使用区块编辑器提供的默认渐变或通过 `editor-gradient-presets` 主题支持设置提供的渐变。
### 禁用基础布局样式
_**注意:**自 WordPress 6.1 起。_
主题可以选择退出生成的区块布局样式,这些样式为核心区块(包括群组、列、按钮和社交图标)提供默认结构样式。通过使用以下代码,这些主题承诺提供自己的结构样式,因为使用此功能将导致核心区块在编辑器和站点前端显示不正确:
```php
add_theme_support( 'disable-layout-styles' );
```
对于希望自定义 `blockGap` 样式或区块间距的主题,请参阅[关于全局设置和样式的开发者文档](/docs/how-to-guides/themes/global-settings-and-styles.md#what-is-blockgap-and-how-can-i-use-it)。
### 支持自定义行高
某些区块(如段落和标题)支持自定义行高。主题可通过以下代码启用对此功能的支持:
```php
add_theme_support( 'custom-line-height' );
```
### 宽对齐与浮动元素
要创建一个能够同时适应宽幅图像、侧边栏、居中列以及限定在居中列内的浮动元素的响应式布局,可能会遇到一些困难。
区块编辑器为浮动图像添加了额外的标记,以便更轻松地设置样式。
以下是一个带标题的 `Image` 标记示例:
```html
<figure class="wp-block-image">
<img src="..." alt="" width="200px" />
<figcaption>简短图片标题。</figcaption>
</figure>
```
以下是一个左浮动图像的标记示例:
```html
<div class="wp-block-image">
<figure class="alignleft">
<img src="..." alt="" width="200px" />
<figcaption>简短图片标题。</figcaption>
</figure>
</div>
```
这里有一个使用上述标记实现响应式布局的 [codepen](https://codepen.io/joen/pen/zLWvrW) 示例,该布局包含侧边栏、宽幅图像以及带有限定标题的浮动元素。
### 区块调色板
不同区块可以自定义颜色。区块编辑器提供了默认调色板,但主题可以覆盖它并提供自己的调色板:
```php
add_theme_support( 'editor-color-palette', array(
array(
'name' => esc_attr__( '强品红色', 'themeLangDomain' ),
'slug' => 'strong-magenta',
'color' => '#a156b4',
),
array(
'name' => esc_attr__( '浅灰品红色', 'themeLangDomain' ),
'slug' => 'light-grayish-magenta',
'color' => '#d0a5db',
),
array(
'name' => esc_attr__( '极浅灰色', 'themeLangDomain' ),
'slug' => 'very-light-gray',
'color' => '#eee',
),
array(
'name' => esc_attr__( '极深灰色', 'themeLangDomain' ),
'slug' => 'very-dark-gray',
'color' => '#444',
),
) );
```
`name` 是一个易于理解的标签(如上所示),它会显示在工具提示中,并为用户提供颜色的有意义的描述。这对于依赖屏幕阅读器或难以感知颜色的用户尤其重要。`slug` 是颜色的唯一标识符,用于生成区块编辑器调色板的 CSS 类。`color` 是指定颜色的十六进制代码。
某些颜色会动态变化,例如“主色”和“辅色”,例如在 Twenty Nineteen 主题中,这些颜色无法通过编程方式描述。尽管如此,建议在适用时为至少默认值提供有意义的 `name`
颜色将按顺序显示在调色板上,并且可以指定的颜色数量没有限制。
主题负责创建在不同上下文中应用颜色的类。核心区块使用“颜色”、“背景颜色”和“边框颜色”上下文。因此为了正确地将“强品红色”应用于核心区块的所有上下文主题应自行实现这些类。类名的构建方式是在“has-”后附加使用短横线命名法的类名,并以上下文名称结尾。
```css
.has-strong-magenta-color {
color: #a156b4;
}
.has-strong-magenta-background-color {
background-color: #a156b4;
}
.has-strong-magenta-border-color {
border-color: #a156b4;
}
```
从 WordPress 5.9 开始,要覆盖核心定义的颜色值,没有 `theme.json` 的主题必须通过 CSS 自定义属性设置其值而不是提供类。CSS 自定义属性使用以下命名方式:`--wp--preset--color--<slug>`。更多信息请参阅 [此开发说明](https://make.wordpress.org/core/2022/01/08/updates-for-settings-styles-and-theme-json/)。例如:
```css
:root {
--wp--preset--color--cyan-bluish-gray: <>;
--wp--preset--color--pale-pink: <>;
}
```
### 区块渐变预设
不同区块可以从预定义的渐变列表中选择。区块编辑器提供了默认的渐变预设,但主题可以覆盖它们并提供自己的预设:
```php
add_theme_support(
'editor-gradient-presets',
array(
array(
'name' => esc_attr__( '鲜艳的蓝绿色到鲜艳的紫色', 'themeLangDomain' ),
'gradient' => 'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)',
'slug' => 'vivid-cyan-blue-to-vivid-purple'
),
array(
'name' => esc_attr__( '鲜艳的绿青色到鲜艳的蓝绿色', 'themeLangDomain' ),
'gradient' => 'linear-gradient(135deg,rgba(0,208,132,1) 0%,rgba(6,147,227,1) 100%)',
'slug' => 'vivid-green-cyan-to-vivid-cyan-blue',
),
array(
'name' => esc_attr__( '浅绿青色到鲜艳的绿青色', 'themeLangDomain' ),
'gradient' => 'linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%)',
'slug' => 'light-green-cyan-to-vivid-green-cyan',
),
array(
'name' => esc_attr__( '明亮鲜艳的琥珀色到明亮鲜艳的橙色', 'themeLangDomain' ),
'gradient' => 'linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%)',
'slug' => 'luminous-vivid-amber-to-luminous-vivid-orange',
),
array(
'name' => esc_attr__( '明亮鲜艳的橙色到鲜艳的红色', 'themeLangDomain' ),
'gradient' => 'linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%)',
'slug' => 'luminous-vivid-orange-to-vivid-red',
),
)
);
```
`name` 是一个易于理解的标签(如上所示),它会显示在工具提示中,并为用户提供渐变的有意义的描述。这对于依赖屏幕阅读器或难以感知颜色的用户尤其重要。`gradient` 是应用于区块背景图像的渐变的 CSS 值。有效渐变类型的详细信息可以在 [mozilla 文档](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Images/Using_CSS_gradients) 中找到。`slug` 是渐变的唯一标识符,用于生成区块编辑器使用的 CSS 类。
主题负责创建应用渐变的类。因此,要正确应用“鲜艳的蓝绿色到鲜艳的紫色”,主题应实现以下类:
```css
.has-vivid-cyan-blue-to-vivid-purple-gradient-background {
background: linear-gradient(
135deg,
rgba( 6, 147, 227, 1 ) 0%,
rgb( 155, 81, 224 ) 100%
);
}
```
从 WordPress 5.9 开始,要覆盖核心定义的渐变值,没有 `theme.json` 的主题必须通过 CSS 自定义属性设置其值而不是提供类。CSS 自定义属性使用以下命名方式:`--wp--preset--gradient--<slug>`。更多信息请参阅 [此开发说明](https://make.wordpress.org/core/2022/01/08/updates-for-settings-styles-and-theme-json/)。例如:
```css
:root {
--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: <>;
--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: <>;
}
```

225
how-to-guides/thunks.md Normal file
View File

@@ -0,0 +1,225 @@
## Thunks API
thunk 函数接收一个包含以下键的单一对象参数:
### select
一个包含与状态预绑定的存储选择器的对象,这意味着您无需提供状态,只需提供额外参数。`select` 会触发相关的解析器(如果存在),但不会等待其完成。即使当前值为空,它也会直接返回当前值。
如果某个选择器是公共 API 的一部分,它将以方法形式存在于 select 对象上:
```js
const thunk = () => ( { select } ) => {
// select 是该存储选择器的对象,已预绑定到当前状态:
const temperature = select.getTemperature();
}
```
由于并非所有选择器都在存储中公开,`select` 同时支持以函数形式传递选择器作为参数:
```js
const thunk = () => ( { select } ) => {
// select 支持私有选择器:
const doubleTemperature = select( ( temperature ) => temperature * 2 );
}
```
### resolveSelect
`resolveSelect``select` 相同,但它返回一个 Promise该 Promise 会通过相关解析器提供的值进行解析。
```js
const thunk = () => ( { resolveSelect } ) => {
const temperature = await resolveSelect.getTemperature();
}
```
### dispatch
一个包含存储操作的对象
如果某个操作是公共 API 的一部分,它将以方法形式存在于 `dispatch` 对象上:
```js
const thunk = () => ( { dispatch } ) => {
// dispatch 是该存储操作的对象:
const temperature = await dispatch.retrieveTemperature();
}
```
由于并非所有操作都在存储中公开,`dispatch` 同时支持以函数形式传递 Redux 操作作为参数:
```js
const thunk = () => async ( { dispatch } ) => {
// dispatch 也是一个接受内联操作的函数:
dispatch({ type: 'SET_TEMPERATURE', temperature: result.value });
// thunk 可与操作互换使用
dispatch( updateTemperature( 100 ) );
// thunk 也可以是异步的。当它们是异步时dispatch 会返回一个 Promise
await dispatch( ( ) => window.fetch( /* ... */ ) );
}
```
### registry
注册表通过其 `dispatch``select``resolveSelect` 方法提供对其他存储的访问。
这些方法与上述方法非常相似,但略有不同。调用 `registry.select( storeName )` 会返回一个函数,该函数返回来自 `storeName` 的选择器对象。当您需要与另一个存储交互时,这会非常方便。例如:
```js
const thunk = () => ( { registry } ) => {
const error = registry.select( 'core' ).getLastEntitySaveError( 'root', 'menu', menuId );
/* ... */
}
```
# Core-Data 中的 Thunk 函数
[Gutenberg 11.6](https://github.com/WordPress/gutenberg/pull/27276) 新增了对 _thunk_ 的支持。您可以将 thunk 理解为可被调度的函数:
```js
// actions.js
export const myThunkAction = () => ( { select, dispatch } ) => {
return "我是一个 thunk我可以被调度使用选择器甚至调度其他动作。";
};
```
## Thunk 函数的优势何在?
Thunk [拓展了 Redux 动作的定义边界](https://jsnajdr.wordpress.com/2021/10/04/motivation-for-thunks/)。在 thunk 出现之前,动作是纯函数化的,只能返回和生成数据。常见的应用场景(例如在动作中与存储库交互或请求 API 数据)需要使用独立的 [control](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#controls)。您经常会看到这样的代码:
```js
export function* saveRecordAction( id ) {
const record = yield controls.select( 'current-store', 'getRecord', id );
yield { type: 'BEFORE_SAVE', id, record };
const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record });
yield { type: 'AFTER_SAVE', id, results };
return results;
}
const controls = {
select: // ...,
fetch: // ...,
};
```
像存储库操作和 fetch 函数这样的副作用需要在动作之外实现。Thunk 为这种方法提供了替代方案,允许您直接内联处理副作用:
```js
export const saveRecordAction = ( id ) => async ({ select, dispatch }) => {
const record = select( 'current-store', 'getRecord', id );
dispatch({ type: 'BEFORE_SAVE', id, record });
const response = await fetch({ url: 'https://...', method: 'POST', data: record });
const results = await response.json();
dispatch({ type: 'AFTER_SAVE', id, results });
return results;
}
```
这样就无需再单独实现 controls。
### Thunk 可访问存储库辅助工具
让我们看一个 Gutenberg 核心代码中的例子。在 thunk 出现之前,`@wordpress/interface` 包中的 `toggleFeature` 动作是这样实现的:
```js
export function* toggleFeature( scope, featureName ) {
const currentValue = yield controls.select(
interfaceStoreName,
'isFeatureActive',
scope,
featureName
);
yield controls.dispatch(
interfaceStoreName,
'setFeatureValue',
scope,
featureName,
! currentValue
);
}
```
Controls 曾是唯一能调度动作和从存储库选择数据的方式。
通过 thunk现在有了更简洁的实现方式。这是 `toggleFeature` 当前的实现:
```js
export function toggleFeature( scope, featureName ) {
return function ( { select, dispatch } ) {
const currentValue = select.isFeatureActive( scope, featureName );
dispatch.setFeatureValue( scope, featureName, ! currentValue );
};
}
```
借助 `select``dispatch` 参数thunk 可以直接使用存储库,无需依赖生成器和 controls。
### Thunk 支持异步操作
假设有个简单的 React 应用,允许您设置恒温器温度。它只有一个输入框和一个按钮。点击按钮会调用携带输入值的 `saveTemperatureToAPI` 动作。
若使用 controls 保存温度,存储库定义如下所示:
```js
const store = wp.data.createReduxStore( 'my-store', {
actions: {
saveTemperatureToAPI: function*( temperature ) {
const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } };
return result;
}
},
controls: {
async FETCH_JSON( action ) {
const response = await window.fetch( action.url, {
method: action.method,
body: JSON.stringify( action.data ),
} );
return response.json();
}
},
// reducers, selectors, ...
} );
```
虽然代码逻辑清晰,但存在一层间接调用。`saveTemperatureToAPI` 动作并不直接与 API 通信,而是需要通过 `FETCH_JSON` control 中转。
让我们看看如何用 thunk 消除这层间接调用:
```js
const store = wp.data.createReduxStore( 'my-store', {
actions: {
saveTemperatureToAPI: ( temperature ) => async () => {
const response = await window.fetch( 'https://...', {
method: 'POST',
body: JSON.stringify( { temperature } ),
} );
return await response.json();
}
},
// reducers, selectors, ...
} );
```
这非常简洁更棒的是resolvers 也同样支持这种写法:
```js
const store = wp.data.createReduxStore( 'my-store', {
// ...
selectors: {
getTemperature: ( state ) => state.temperature
},
resolvers: {
getTemperature: () => async ( { dispatch } ) => {
const response = await window.fetch( 'https://...' );
const result = await response.json();
dispatch.receiveCurrentTemperature( result.temperature );
}
},
// ...
} );
```
与(现已过时的)生成器和 controls 支持一样,所有数据存储库默认都包含对 thunk 的支持。

View File

@@ -0,0 +1,9 @@
# 小工具
Gutenberg插件将WP管理后台中的小工具编辑器界面替换为基于WordPress区块编辑器的全新界面。
**目录**
- [小工具区块编辑器概述](/docs/how-to-guides/widgets/overview.md)
- [恢复旧版小工具编辑器](/docs/how-to-guides/widgets/opting-out.md)
- [确保与旧版小工具区块的兼容性](/docs/how-to-guides/widgets/legacy-widget-block.md)

View File

@@ -0,0 +1,188 @@
# 关于传统小工具区块
传统小工具区块允许用户添加、编辑和预览由插件注册的第三方小工具,以及使用经典小工具编辑器添加的小工具。
可通过区块插入器添加传统小工具区块,并从该区块的下拉菜单中选择小工具来添加第三方小工具。
也可通过在区块插入器中搜索小工具名称并选择该小工具来添加第三方小工具。系统将插入一个传统小工具区块的变体。
## 与传统小工具区块的兼容性
### `widget-added` 事件
传统小工具区块将以类似于定制器的方式显示小工具的表单,因此与大多数第三方小工具兼容。
如果小工具在其表单中使用 JavaScript则必须在 `document` 上触发 `'widget-added'` jQuery 事件后,将事件添加到 DOM 中。
例如,当“更改密码”复选框被勾选时,小工具可能希望显示一个“密码”字段。
```js
( function ( $ ) {
$( document ).on( 'widget-added', function ( $event, $control ) {
$control.find( '.change-password' ).on( 'change', function () {
var isChecked = $( this ).prop( 'checked' );
$control.find( '.password' ).toggleClass( 'hidden', ! isChecked );
} );
} );
} )( jQuery );
```
请注意,所有小工具的事件处理程序都在 `widget-added` 回调中添加。
### 显示“无预览可用”
当未选择传统小工具区块时,该区块将显示小工具的预览。
当小工具的 `widget()` 函数未呈现任何内容或仅呈现空的 HTML 元素时,传统小工具区块会自动显示“无预览可用”的消息。
小工具可以通过在不应显示预览时从 `widget()` 提前返回来利用这一点。
```php
class ExampleWidget extends WP_Widget {
...
public function widget( $instance ) {
if ( ! isset( $instance['name'] ) ) {
// 名称为必填项,如果没有则什么都不显示。
return;
}
?>
<h3>名称:<?php echo esc_html( $instance['name'] ); ?></h3>
...
<?php
}
...
}
```
### 允许迁移到区块
您可以允许用户轻松将包含特定小工具的传统小工具区块迁移到一个或多个区块。这使得插件作者可以逐步淘汰他们的小工具,转而使用更直观且可以在更多地方使用的区块。
以下步骤展示了如何实现这一点。
#### 1) 在 REST API 中显示小工具的实例
首先,我们需要告诉 WordPress 允许在 REST API 中显示您的小工具的实例数组。
如果满足以下条件,则可以安全地执行此操作:
- 您知道小工具在 `$instance` 中存储的所有值都可以用 JSON 表示;并且
- 您知道小工具没有在 `$instance` 中存储任何应对具有站点定制权限的用户隐藏的私有数据。
如果这样做是安全的,则在注册小工具时包含一个名为 `show_instance_in_rest` 的小工具选项,并将其值设置为 `true`
```php
class ExampleWidget extends WP_Widget {
...
/**
* 设置小工具
*/
public function __construct() {
$widget_ops = array(
// ...其他选项
'show_instance_in_rest' => true,
// ...其他选项
);
parent::__construct( 'example_widget', 'ExampleWidget', $widget_ops );
}
...
}
```
这允许区块编辑器和其他 REST API 客户端通过访问 REST API 响应中的 `instance.raw` 来查看您的小工具的实例数组。
请注意,[WordPress 5.8.0 之前的版本允许您通过在扩展 `WP_Widget` 的类中将 `$show_instance_in_rest` 设置为 `true`](https://core.trac.wordpress.org/ticket/53332) 来启用此功能。
```php
class ExampleWidget extends WP_Widget {
...
public $show_instance_in_rest = true;
...
}
```
现在已不推荐使用此方法,建议改用小工具选项方法。
#### 2) 添加区块转换
现在,我们可以定义一个[区块转换](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-transforms/),告诉区块编辑器用什么替换包含您小工具的传统小工具区块。
这是通过向区块定义中添加 JavaScript 代码来实现的。在此示例中,我们定义了一个转换,将 ID 为 `'example_widget'` 的小工具转换为名称为 `'example/block'` 的区块。
```js
transforms: {
from: [
{
type: 'block',
blocks: [ 'core/legacy-widget' ],
isMatch: ( { idBase, instance } ) => {
if ( ! instance?.raw ) {
// 如果原始实例未在 REST API 中显示,则无法转换。
return false;
}
return idBase === 'example_widget';
},
transform: ( { instance } ) => {
return createBlock( 'example/block', {
name: instance.raw.name,
} );
},
},
]
},
```
#### 3) 在传统小工具区块中隐藏小工具
最后,我们可以告诉传统小工具区块在“选择小工具”下拉列表和区块插入器中隐藏您的小工具。这鼓励用户使用替换您小工具的区块。
可以使用 `widget_types_to_hide_from_legacy_widget_block` 过滤器来实现这一点。
```php
function hide_example_widget( $widget_types ) {
$widget_types[] = 'example_widget';
return $widget_types;
}
add_filter( 'widget_types_to_hide_from_legacy_widget_block', 'hide_example_widget' );
```
## 在其他区块编辑器中使用传统小工具区块(高级)
您可以选择允许在其他区块编辑器(例如 WordPress 文章编辑器)中使用传统小工具区块。默认情况下,此功能未启用。
首先,确保页面上加载了传统小工具所需的任何样式和脚本。一种便捷的方法是手动执行用户浏览小工具 WP 管理屏幕时通常运行的所有钩子。
```php
add_action( 'admin_print_styles', function() {
if ( get_current_screen()->is_block_editor() ) {
do_action( 'admin_print_styles-widgets.php' );
}
} );
add_action( 'admin_print_scripts', function() {
if ( get_current_screen()->is_block_editor() ) {
do_action( 'load-widgets.php' );
do_action( 'widgets.php' );
do_action( 'sidebar_admin_setup' );
do_action( 'admin_print_scripts-widgets.php' );
}
} );
add_action( 'admin_print_footer_scripts', function() {
if ( get_current_screen()->is_block_editor() ) {
do_action( 'admin_print_footer_scripts-widgets.php' );
}
} );
add_action( 'admin_footer', function() {
if ( get_current_screen()->is_block_editor() ) {
do_action( 'admin_footer-widgets.php' );
}
} );
```
然后,使用 `@wordpress/widgets` 包中定义的 `registerLegacyWidgetBlock` 注册传统小工具区块。
```php
add_action( 'enqueue_block_editor_assets', function() {
wp_enqueue_script( 'wp-widgets' );
wp_add_inline_script( 'wp-widgets', 'wp.widgets.registerLegacyWidgetBlock()' );
} );
```

View File

@@ -0,0 +1,44 @@
# 恢复经典小工具编辑器
有多种方法可以禁用新的小工具区块编辑器。
## 使用 `remove_theme_support`
主题可以通过调用 `remove_theme_support( 'widgets-block-editor' )` 来禁用小工具区块编辑器。
例如,主题可以在 `functions.php` 文件中添加以下 PHP 代码:
```php
function example_theme_support() {
remove_theme_support( 'widgets-block-editor' );
}
add_action( 'after_setup_theme', 'example_theme_support' );
```
## 使用经典小工具插件
最终用户可以通过安装并激活[经典小工具插件](https://wordpress.org/plugins/classic-widgets/)来禁用小工具区块编辑器。
安装此插件后,可以通过停用和激活插件来切换小工具区块编辑器的开关状态。
## 使用过滤器
`use_widgets_block_editor` 过滤器用于控制是否启用小工具区块编辑器。
例如,网站管理员可以在 Must-Use 插件中添加以下 PHP 代码来禁用小工具区块编辑器:
```php
add_filter( 'use_widgets_block_editor', '__return_false' );
```
对于更高级的用法,您可以提供自定义函数。以下示例展示了如何为特定用户禁用小工具区块编辑器:
```php
function example_use_widgets_block_editor( $use_widgets_block_editor ) {
if ( 123 === get_current_user_id() ) {
return false;
}
return $use_widgets_block_editor;
}
add_filter( 'use_widgets_block_editor', 'example_use_widgets_block_editor' );
```

View File

@@ -0,0 +1,31 @@
# 小工具区块编辑器概述
## 小工具区块编辑器
全新小工具编辑器是WordPress的一项功能升级它将小工具区域升级为支持区块与小工具并存使用。这项功能通过大家熟悉的WordPress区块编辑器提供了全新小工具管理体验。
您可以通过访问外观→小工具或外观→自定义→小工具并选择小工具区域,来使用全新小工具编辑器。
小工具区块编辑器允许您通过独立编辑器或自定义器,将区块和小工具插入当前启用主题定义的任意[小工具区域或侧边栏](https://developer.wordpress.org/themes/functionality/sidebars/)。
### 自定义器小工具区块编辑器
全新小工具编辑器同时取代了自定义器中的小工具版块,采用基于区块的全新编辑器。
您可以通过访问外观→自定义,选择小工具,然后选择小工具区域来使用自定义器小工具区块编辑器。
通过自定义器使用全新小工具编辑器不仅支持将区块和小工具插入选定的小工具区域,还能利用编辑器右侧的实时预览功能,以及所有其他自定义器专属功能(如变更排程与共享)。
## 兼容性
在新版小工具编辑器推出前已添加到小工具区域的传统小工具,将通过传统小工具区块继续正常工作。
传统小工具区块作为兼容机制,允许我们在基于区块的全新小工具编辑器中编辑和预览传统小工具的变更。
通过插件注册的第三方小工具仍可通过添加和设置传统小工具区块,插入到小工具区域中。
小工具编辑器使用用户不可见的底层“区块”小工具来存储区块数据。这意味着插件和主题将继续正常工作,且停用小工具区块编辑器不会造成任何数据丢失。
主题可通过`remove_theme_support( 'widgets-block-editor' )`代码禁用小工具区块编辑器。
用户可通过安装[经典小工具插件](https://wordpress.org/plugins/classic-widgets/)来禁用小工具区块编辑器。