25 KiB
总结
恭喜您完成本指南的学习!现在您应该对区块编辑器的工作原理有了更深入的理解。
您刚刚构建的自定义区块编辑器完整代码已发布于GitHub,欢迎下载并亲自体验。在实践操作中不断探索,将所学知识推向新的高度。
区块持久化
在创建自定义区块编辑器的旅程中,您已经取得了长足进展。但还有一个重要领域有待探讨——区块持久化。换句话说,就是让您的区块在页面刷新之间保持保存并可用。
由于这仅是一项实验,本指南选择使用浏览器的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。同时,它还将区块存储在LocalStorage中,键名为getdavesbeBlocks。为实现这一点,区块数据被序列化为Gutenberg"区块语法"格式,这意味着可以安全地将其存储为字符串。
如果打开开发者工具并检查LocalStorage,您会看到序列化的区块数据随着编辑器中发生的更改而存储和更新。以下是该格式的示例:
<!-- wp:heading -->
<h2>在WordPress后台进行独立区块编辑器的实验</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>本实验旨在探索在WordPress后台创建独立区块编辑器实例的难易程度。</p>
<!-- /wp:paragraph -->
检索历史区块数据
实现持久化固然重要,但只有当这些数据在每次完整页面重载时被检索并在编辑器中恢复,才能真正发挥作用。
访问数据会产生副作用,因此必须使用useEffect钩子来处理:
// 文件: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 包驱动,这是一个提供编辑器核心功能的 JavaScript 库。
该软件包还可用于为几乎任何其他网络应用程序创建自定义区块编辑器。这意味着您可以在 WordPress 之外使用相同的区块和区块编辑体验。
这种灵活性和互操作性使区块成为跨多个应用程序构建和管理内容的强大工具。同时也让开发人员能更轻松地创建最适合用户的内容编辑器。
本指南将介绍创建首个自定义区块编辑器的基础知识。
引言
尽管古腾堡代码库包含众多软件包和组件,初看可能令人望而生畏。但其核心始终是管理和编辑区块。因此若想在编辑器上进行开发,必须从基础层面理解区块编辑的工作原理。
本指南将引导您在 WordPress 内构建一个功能完整的自定义区块编辑器"实例"。在此过程中,我们将向您介绍关键软件包和组件,帮助您洞悉区块编辑器的工作机制。
阅读完本文后,您将对区块编辑器的内部运作有扎实的理解,并为创建自己的区块编辑器实例奠定坚实基础。
代码语法
本指南中的代码片段使用 JSX 语法。当然您也可以选择使用原生 JavaScript。不过许多开发者在熟悉 JSX 后认为其更易读写,因此《区块编辑器手册》中的所有代码示例均采用此语法。
即将构建的内容
通过本指南,您将创建一个(近乎)功能完整的区块编辑器实例。最终效果如下所示:
虽然外观相似,但这个编辑器并非您在WordPress中创建文章和页面时熟悉的那个区块编辑器,而是一个完全自定义的实例,将存在于名为"区块编辑器"的自定义WordPress管理页面中。
该编辑器将具备以下特性:
- 支持添加和编辑所有核心区块
- 熟悉的视觉样式和主界面/侧边栏布局
- 页面刷新后保持基础的区块持久化
插件设置与组织架构
自定义编辑器将以WordPress插件形式构建。为简化概念,插件将命名为 Standalone Block Editor Demo(独立区块编辑器演示),这正是其功能体现。
插件文件结构如下所示:
以下是核心文件说明:
plugin.php– 包含注释元数据的标准插件入口文件,负责引入init.phpinit.php- 处理主插件逻辑的初始化src/(目录) - 存放 JavaScript 和 CSS 源文件的位置(这些文件不由插件直接加载)webpack.config.js- 自定义 Webpack 配置,扩展了@wordpress/scriptsnpm 包提供的默认配置,以支持自定义CSS样式(通过Sass实现)
未在上图显示的是 build/ 目录,该目录用于存放通过 @wordpress/scripts 编译输出的 JS 和 CSS 文件。这些文件由插件单独加载。
基础文件结构就绪后,接下来让我们了解需要哪些软件包。
键盘导航
完成基础组件结构搭建后,最后一步是将所有内容包裹在 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>,下面我们来详细分析。
理解 <BlockEditorProvider> 组件
<BlockEditorProvider> 是组件层级中最重要的组件之一。它为新区块编辑器建立了一个独立的区块编辑上下文环境。
因此,该组件对本项目的核心目标具有奠基性意义。
<BlockEditorProvider> 的子组件构成了区块编辑器的用户界面。这些组件通过上下文(Context)获取数据,从而能够在编辑器内渲染和管理区块及其行为。
// 文件:src/components/block-editor/index.js
<BlockEditorProvider
value={ blocks } // 区块对象数组
onInput={ updateBlocks } // 处理区块更新的回调函数
onChange={ persistBlocks } // 处理区块更新/持久化的回调函数
settings={ settings } // 编辑器"设置"对象
/>
BlockEditor 属性说明
可以看到 <BlockEditorProvider> 接收一个(已解析的)区块对象数组作为其 value 属性。当检测到编辑器内部发生变更时,会调用 onChange 和/或 onInput 处理函数(将新的区块数组作为参数传递)。
其内部实现机制是通过订阅提供的注册表(通过 withRegistryProvider 高阶组件),监听区块变更事件,判断区块变更是否具有持久性,然后相应调用合适的 onChange|Input 处理函数。
在这个简单项目中,这些功能使您可以实现:
- 将当前区块数组作为状态存储于
blocks - 通过调用钩子设置器
updateBlocks(blocks),在onInput时更新内存中的blocks状态 - 使用
onChange将区块基础数据持久化到localStorage(该事件在区块更新被确认为"已提交"时触发)
需要特别注意的是,该组件还接收 settings 属性。这里将配置之前在 init.php 中以 JSON 格式内联的编辑器设置,可用于配置自定义颜色、可用图片尺寸等功能,以及更多设置。
编辑器的“核心”
虽然 WordPress 编辑器由众多动态组件构成,但其核心是 @wordpress/block-editor 包,该包的功能可通过其自述文件精辟概括:
此模块允许您创建并使用独立的区块编辑器。
完美!这正是您用来创建自定义区块编辑器实例的核心工具包。但首先,您需要为编辑器创建载体。
创建自定义“区块编辑器”页面
让我们从在 WordPress 后台创建自定义页面开始,该页面将承载自定义区块编辑器实例。
注册页面
您需要使用标准的 WordPress add_menu_page() 辅助函数来注册自定义后台页面:
// 文件:init.php
add_menu_page(
'独立区块编辑器', // 页面可见名称
'区块编辑器', // 菜单标签
'edit_posts', // 所需权限
'getdavesbe', // 页面钩子/别名
'getdave_sbe_render_block_editor', // 页面渲染函数
'dashicons-welcome-widgets-menus' // 自定义图标
);
getdave_sbe_render_block_editor 函数将用于渲染后台页面的内容。温馨提示:每个步骤的源代码均可在配套插件中查看。
添加目标 HTML
由于区块编辑器是基于 React 的应用程序,您需要在自定义页面中输出 HTML 容器,以便 JavaScript 渲染区块编辑器。
让我们使用上一步中引用的 getdave_sbe_render_block_editor 函数:
// 文件: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 钩子。
首先需确保自定义代码仅运行在目标后台页面。在回调函数开头添加页面标识校验:
// 文件: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() 安全注册主 JavaScript 文件:
// 文件:init.php
wp_enqueue_script( $script_handle, $script_url, $script_asset['dependencies'], $script_asset['version'] );
为节省篇幅,此处省略了 $script_ 变量的赋值过程,您可在此查看完整代码。
注意第三个参数 $script_asset['dependencies'],这些依赖项通过 @wordpress/dependency-extraction-webpack-plugin 动态生成,该工具会确保 WordPress 提供的脚本不会打包到构建文件中。
同时需要注册自定义 CSS 样式和 WordPress 默认格式化库,以应用优美的默认样式:
// 文件: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 包时,您会发现它接受一个设置对象来配置编辑器的默认设置。这些设置在服务器端可用,因此您需要将它们暴露给 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>– 在编辑器 UI 顶部渲染静态标题“独立块编辑器”<BlockEditor>– 自定义块编辑器组件
理解 <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 包中的组件是最复杂且涉及最多的组件之一。如果你想从根本上理解编辑器的工作原理,理解这些组件至关重要。强烈建议研究这些组件。
审查侧边栏
在 <BlockEditor> 的渲染中,还有一个 <Sidebar> 组件。
// 文件:src/components/block-editor/index.js
return (
<div className="getdavesbe-block-editor">
<BlockEditorProvider>
<Sidebar.InspectorFill> /* <-- 侧边栏 */
<BlockInspector />
</Sidebar.InspectorFill>
<BlockCanvas height="400px" />
</BlockEditorProvider>
</div>
);
这部分用于通过 <BlockInspector> 组件显示高级区块设置。
<Sidebar.InspectorFill>
<BlockInspector />
</Sidebar.InspectorFill>
然而,细心的读者可能已经注意到,在 <Editor>(src/editor.js)组件的布局中已经存在一个 <Sidebar> 组件:
// 文件: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> 的渲染过程。
<BlockInspector>
本身实际上渲染了一个用于 <InspectorControls> 的 Slot。这使你可以在区块的 edit() 定义中渲染一个 <InspectorControls>> 组件,并在编辑器的侧边栏中显示它。建议进一步探索该组件的更多细节。


