title: "构建自定义区块编辑器" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - Platform - How To Guides - Repos
构建自定义区块编辑器
WordPress 区块编辑器是一款强大的工具,允许您以多种方式创建和格式化内容。它部分由 @wordpress/block-editor 包驱动,这是一个提供编辑器核心功能的 JavaScript 库。
该包也可用于为几乎任何其他 Web 应用程序创建自定义区块编辑器。这意味着您可以在 WordPress 之外使用相同的区块和区块编辑体验。

这种灵活性和互操作性使区块成为跨多个应用程序构建和管理内容的强大工具。它也使开发人员能够更轻松地创建最适合其用户的内容编辑器。
简介
Gutenberg 代码库包含众多包和组件,初看可能令人望而生畏。但其核心始终围绕着管理和编辑区块。因此,若想在编辑器上开展工作,从基础层面理解区块编辑的工作原理至关重要。
本指南将引导您在 WordPress 内构建一个功能完整的自定义区块编辑器“实例”。在此过程中,我们将向您介绍关键的包和组件,以便您了解区块编辑器在底层是如何运作的。
阅读完本文后,您将对区块编辑器的内部机制有扎实的理解,并能顺利创建自己的区块编辑器实例。
代码语法
本指南中的代码片段使用 JSX 语法。当然,您也可以选择使用原生 JavaScript。不过,许多开发者在熟悉 JSX 后,会发现其更易于阅读和编写,因此区块编辑器手册中的所有代码示例均采用此语法。
你将构建什么
在本指南中,你将创建一个(几乎)功能完整的区块编辑器实例。最终效果将类似于:

虽然看起来相似,但这个编辑器并非你在 WordPress 中创建文章和页面时熟悉的区块编辑器。相反,它将是一个完全自定义的实例,位于名为“区块编辑器”的自定义 WordPress 管理页面中。
该编辑器将具备以下功能:
- 能够添加和编辑所有核心区块。
- 熟悉的视觉样式和主界面/侧边栏布局。
- 页面重新加载时区块的基本持久化保存。
Plugin setup and organization
The custom editor is going to be built as a WordPress plugin. To keep things simple, the plugin will be named Standalone Block Editor Demo because that is what it does.
The plugin file structure will look like this:

Here is a brief summary of what's going on:
plugin.php– Standard plugin "entry" file with comment meta data, which requiresinit.php.init.php- Handles the initialization of the main plugin logic.src/(directory) - This is where the JavaScript and CSS source files will live. These files are not directly enqueued by the plugin.webpack.config.js- A custom Webpack config extending the defaults provided by the@wordpress/scriptsnpm package to allow for custom CSS styles (via Sass).
The only item not shown above is the build/ directory, which is where the compiled JS and CSS files are outputted by @wordpress/scripts. These files are enqueued by the plugin separately.
With the basic file structure in place, let's look at what packages will be needed.
编辑器的“核心”
虽然 WordPress 编辑器由许多活动部件组成,但其核心是 @wordpress/block-editor 包,其自述文件对此做了最佳总结:
此模块允许您创建和使用独立的块编辑器。
很好,这是您将用来创建自定义块编辑器实例的主要包。但首先,您需要为编辑器创建一个“家”。
创建自定义“区块编辑器”页面
让我们从在 WordPress 后台创建一个自定义页面开始,该页面将承载自定义区块编辑器实例。
注册页面
为此,你需要使用标准的 WordPress add_menu_page() 辅助函数来注册一个自定义管理页面:
// 文件:init.php
add_menu_page(
'Standalone Block Editor', // 可见的页面名称
'Block Editor', // 菜单标签
'edit_posts', // 所需权限
'getdavesbe', // 页面钩子/别名
'getdave_sbe_render_block_editor', // 用于渲染页面的函数
'dashicons-welcome-widgets-menus' // 自定义图标
);
getdave_sbe_render_block_editor 函数将用于渲染管理页面的内容。提醒一下,每个步骤的源代码都可以在配套插件中找到。
Adding the target HTML
Since the block editor is a React-powered application, you need to output some HTML into the custom page where JavaScript can render the block editor.
Let's use the getdave_sbe_render_block_editor function referenced in the step above.
// File: init.php
function getdave_sbe_render_block_editor() {
?>
<div
id="getdave-sbe-block-editor"
class="getdave-sbe-block-editor"
>
Loading Editor...
</div>
<?php
}
The function outputs some basic placeholder HTML. Note the id attribute getdave-sbe-block-editor, which will be used shortly.
Enqueuing JavaScript and CSS
With the target HTML in place, you can now enqueue some JavaScript and CSS so that they will run on the custom admin page.
To do this, let's hook into admin_enqueue_scripts.
First, you must ensure the custom code is only run on the custom admin page. So, at the top of the callback function, exit early if the page doesn't match the page's identifier:
// File: init.php
function getdave_sbe_block_editor_init( $hook ) {
// Exit if not the correct page.
if ( 'toplevel_page_getdavesbe' !== $hook ) {
return;
}
}
add_action( 'admin_enqueue_scripts', 'getdave_sbe_block_editor_init' );
With this in place, you can then safely register the main JavaScript file using the standard WordPress wp_enqueue_script() function:
// File: init.php
wp_enqueue_script( $script_handle, $script_url, $script_asset['dependencies'], $script_asset['version'] );
To save time and space, the $script_ variables assignment has been omitted. You can review these here.
Note the third argument for script dependencies, $script_asset['dependencies']. These dependencies are
dynamically generated using @wordpress/dependency-extraction-webpack-plugin which will
ensure that WordPress provided scripts are not included in the built
bundle.
You also need to register both your custom CSS styles and the WordPress default formatting library to take advantage of some nice default styling:
// File: init.php
// Enqueue default editor styles.
wp_enqueue_style( 'wp-format-library' );
// Enqueue custom styles.
wp_enqueue_style(
'getdave-sbe-styles', // Handle
plugins_url( 'build/index.css', __FILE__ ), // Block editor CSS
array( 'wp-edit-blocks' ), // Dependency to include the CSS after it
filemtime( __DIR__ . '/build/index.css' )
);
内联编辑器设置
查看 @wordpress/block-editor 包时,你会发现它接受一个设置对象来配置编辑器的默认设置。这些设置在服务器端可用,因此你需要将它们暴露出来以便在 JavaScript 中使用。
为此,让我们将设置对象内联为 JSON,并赋值给全局的 window.getdaveSbeSettings 对象:
// 文件: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 需要扩展默认的 @wordpress/scripts Webpack 配置。
// 文件: 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>中。
domReady( function () {
const root = createRoot( document.getElementById( 'getdave-sbe-block-editor' ) );
const settings = window.getdaveSbeSettings || {};
registerCoreBlocks();
root.render(
<Editor settings={ settings } />
);
} );
审查 <Editor> 组件
让我们仔细看看上面代码中使用的 <Editor> 组件,它位于配套插件的 src/editor.js 文件中。
尽管名为编辑器,但这并非区块编辑器的实际核心。相反,它是一个包装器组件,用于容纳构成自定义编辑器主体的各个组件。
依赖项
在 <Editor> 内部首先要做的是引入一些依赖项。
// 文件: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> 组件。
// 文件:src/editor.js
function Editor( { settings } ) {
return (
<DropZoneProvider>
<div className="getdavesbe-block-editor-layout">
<Notices />
<Header />
<Sidebar />
<BlockEditor settings={ settings } />
</div>
</DropZoneProvider>
);
}
在这个过程中,编辑器布局的核心结构被搭建起来,同时包含了一些专门的上下文提供者,这些提供者使得特定功能能够在整个组件层次结构中可用。
让我们更详细地检查这些组件:
<DropZoneProvider>– 启用用于拖放功能的放置区域<Notices>– 提供一个“消息条”通知,如果有任何消息被分发到core/notices存储,该通知将被渲染<Header>– 在编辑器界面的顶部渲染静态标题“独立区块编辑器”<BlockEditor>– 自定义的区块编辑器组件
键盘导航
完成这个基础组件结构后,唯一剩下的工作就是将一切包裹在 navigateRegions 高阶组件 中,以便在布局的不同“区域”之间提供键盘导航。
// 文件:src/editor.js
export default navigateRegions( Editor );
自定义 <BlockEditor>
现在核心布局和组件都已就位。是时候探索区块编辑器本身的自定义实现了。
这个组件名为 <BlockEditor>,魔法就在这里发生。
打开 src/components/block-editor/index.js 会发现,这是目前遇到的最复杂的组件。内容很多,所以先从关注 <BlockEditor> 组件渲染的内容开始:
// 文件: 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>。我们来仔细看看它们。
Understanding the <BlockEditorProvider> component
<BlockEditorProvider> is one of the most important components in the hierarchy. It establishes a new block editing context for a new block editor.
As a result, it is fundamental to the entire goal of this project.
The children of <BlockEditorProvider> comprise the UI for the block editor. These components then have access to data (via Context), enabling them to render and manage the blocks and their behaviors within the editor.
// File: src/components/block-editor/index.js
<BlockEditorProvider
value={ blocks } // Array of block objects
onInput={ updateBlocks } // Handler to manage Block updates
onChange={ persistBlocks } // Handler to manage Block updates/persistence
settings={ settings } // Editor "settings" object
/>
BlockEditor 属性
可以看到 <BlockEditorProvider> 接受一个(已解析的)区块对象数组作为其 value 属性,并在检测到编辑器内部发生更改时,调用 onChange 和/或 onInput 处理函数属性(将新的区块作为参数传递)。
其内部实现是通过订阅提供的 registry(通过 withRegistryProvider 高阶组件),监听区块变更事件,判断区块变更是否持久,然后相应地调用适当的 onChange|Input 处理函数。
对于这个简单项目而言,这些功能允许你:
- 将当前区块数组作为
blocks存储在状态中。 - 在
onInput时通过调用钩子设置器updateBlocks(blocks)来更新内存中的blocks状态。 - 使用
onChange将区块基本持久化到localStorage中。这会在区块更新被视为“已提交”时触发。
同样值得回顾的是,该组件接受一个 settings 属性。你可以在此处添加之前在 init.php 中以内联 JSON 形式添加的编辑器设置。这些设置可用于配置自定义颜色、可用图像尺寸等特性,以及更多功能。
理解 <BlockList> 组件
除了 <BlockEditorProvider> 之外,下一个最有趣的组件是 <BlockList>。
这是最重要的组件之一,因为它的作用是将区块列表渲染到编辑器中。
它能够实现这一功能,部分原因在于它被放置为 <BlockEditorProvider> 的子组件,这使其能够完全访问编辑器中当前区块状态的所有信息。
BlockList 如何工作?
在底层,<BlockList> 依赖于其他几个较低层级的组件来渲染区块列表。
这些组件的层级结构可以近似表示如下:
// 仅为示例目的的伪代码。
<BlockList>
/* 从 rootClientId 渲染区块列表。 */
<BlockListBlock>
/* 从 BlockList 渲染单个区块。 */
<BlockEdit>
/* 渲染区块的标准可编辑区域。 */
<Component /> /* 根据其 `edit()` 实现渲染区块 UI。
*/
</BlockEdit>
</BlockListBlock>
</BlockList>
以下是这些组件如何协同工作以渲染区块列表的大致过程:
<BlockList>遍历所有区块的clientIds,并通过<BlockListBlock />渲染每个区块。<BlockListBlock />接着使用其自身的子组件<BlockEdit>渲染单个区块。- 最后,使用
Component占位符组件渲染区块本身。
@wordpress/block-editor 包中的组件是最复杂且涉及面最广的组件之一。如果你想从根本上理解编辑器的工作原理,理解这些组件至关重要。强烈建议深入研究这些组件。
Reviewing the sidebar
Also within the render of the <BlockEditor>, is the <Sidebar> component.
// File: src/components/block-editor/index.js
return (
<div className="getdavesbe-block-editor">
<BlockEditorProvider>
<Sidebar.InspectorFill> /* <-- SIDEBAR */
<BlockInspector />
</Sidebar.InspectorFill>
<BlockCanvas height="400px" />
</BlockEditorProvider>
</div>
);
This is used, in part, to display advanced block settings via the <BlockInspector> component.
<Sidebar.InspectorFill>
<BlockInspector />
</Sidebar.InspectorFill>
However, the keen-eyed readers amongst you will have already noted the presence of a <Sidebar> component within the <Editor> (src/editor.js) component's
layout:
// File: src/editor.js
<Notices />
<Header />
<Sidebar /> // <-- What's this?
<BlockEditor settings={ settings } />
Opening the src/components/sidebar/index.js file, you can see that this is, in fact, the component rendered within <Editor> above. However, the implementation utilises
Slot/Fill to expose a Fill (<Sidebar.InspectorFill>), which is subsequently imported and rendered inside of the <BlockEditor> component (see above).
With this in place, you then can render <BlockInspector /> as a child of the Sidebar.InspectorFill. This has the result of allowing you to keep <BlockInspector> within the React context of <BlockEditorProvider> whilst allowing it to be rendered into the DOM in a separate location (i.e. in the <Sidebar>).
This might seem overly complex, but it is required in order for <BlockInspector> to have access to information about the current block. Without Slot/Fill, this setup would be extremely difficult to achieve.
And with that you have covered the render of you custom <BlockEditor>.
<BlockInspector>
itself actually renders a Slot for <InspectorControls>. This is what allows you render a <InspectorControls>> component inside
the edit() definition for your block and have
it display within the editor's sidebar. Exploring this component in more detail is recommended.
区块持久化
在创建自定义区块编辑器的旅程中,你已经取得了长足的进步。但还有一个重要领域尚未涉及——区块持久化。换句话说,就是让你的区块在页面刷新之间能够被保存并可用。

由于这只是一个实验,本指南选择使用浏览器的 localStorage API 来处理区块数据的保存。在实际场景中,你可能会选择更可靠、更健壮的系统(例如数据库)。
话虽如此,让我们更深入地了解一下如何处理区块的保存。
在状态中存储区块
查看 src/components/block-editor/index.js 文件,你会注意到已创建一些状态来将区块存储为数组:
// 文件:src/components/block-editor/index.js
const [ blocks, updateBlocks ] = useState( [] );
如前所述,blocks 作为 value 属性传递给“受控”组件 <BlockEditorProvider>。这为其“注入”了一组初始区块。同样,updateBlocks 设置器被连接到 <BlockEditorProvider> 的 onInput 回调上,这确保了区块状态与编辑器内对区块所做的更改保持同步。
保存区块数据
现在如果你将注意力转向 onChange 处理程序,会注意到它连接到一个名为 persistBlocks() 的函数,该函数定义如下:
// 文件:src/components/block-editor/index.js
function persistBlocks( newBlocks ) {
updateBlocks( newBlocks );
window.localStorage.setItem( 'getdavesbeBlocks', serialize( newBlocks ) );
}
此函数接收一个"已提交"的区块变更数组,并调用状态设置器 updateBlocks。同时,它还将区块数据以 getdavesbeBlocks 为键存储在 LocalStorage 中。为了实现这一点,区块数据被序列化为 Gutenberg "区块语法" 格式,这意味着它可以安全地以字符串形式存储。
如果你打开开发者工具并检查 LocalStorage,会看到序列化的区块数据随着编辑器中的变更而存储和更新。以下是该格式的示例:
<!-- wp:heading -->
<h2>An experiment with a standalone Block Editor in the WordPress admin</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>This is an experiment to discover how easy (or otherwise) it is to create a standalone instance of the Block Editor in the WordPress admin.</p>
<!-- /wp:paragraph -->
Retrieving previous block data
Having persistence in place is all well and good, but it's only useful if that data is retrieved and restored within the editor upon each full page reload.
Accessing data is a side effect, so you must use the useEffect hook to handle this.
// File: src/components/block-editor/index.js
useEffect( () => {
const storedBlocks = window.localStorage.getItem( 'getdavesbeBlocks' );
if ( storedBlocks && storedBlocks.length ) {
updateBlocks( () => parse( storedBlocks ) );
createInfoNotice( 'Blocks loaded', {
type: 'snackbar',
isDismissible: true,
} );
}
}, [] );
This handler:
- Grabs the serialized block data from local storage.
- Converts the serialized blocks back to JavaScript objects using the
parse()utility. - Calls the state setter
updateBlockscausing theblocksvalue to be updated in state to reflect the blocks retrieved from LocalStorage.
As a result of these operations, the controlled <BlockEditorProvider> component is updated with the blocks restored from LocalStorage, causing the editor to show these blocks.
Finally, you will want to generate a notice - which will display in the <Notice> component as a "snackbar" notice - to indicate that the blocks have been restored.
总结
恭喜你完成本指南。现在你应该对区块编辑器的工作原理有了更深入的理解。
你刚刚构建的自定义区块编辑器的完整代码已发布于 GitHub。下载并亲自尝试,不断实验探索,进一步拓展功能。