我正在开发一个 WordPress 插件,在使用它时存在一定的学习曲线。我想为用户提供一个入门指南,说明如何使用该插件,但我希望避免将用户引导到插件网站上的文档,因为这会打断了他们的体验。
理想情况下,用户在安装插件后可以立即开始使用,并在积极使用过程中获得有用的提示。WordPress 中没有原生功能可以实现这一点,但我们可以自己创建,因为 WordPress 非常灵活。
所以,这就是我们的想法。我们将文档直接嵌入到插件中,并在块编辑器中使其易于访问。这样,用户可以立即使用插件,同时可以直接在工作区域获得常见问题的答案。
我的插件通过多个自定义文章类型 (CPT) 进行操作。我们将要构建的内容本质上是一个弹出模态框,用户在访问这些 CPT 时会看到它。
WordPress 块编辑器是使用 React 构建的,它利用可以针对不同情况进行自定义和重复使用的组件。我们正在构建的内容也是如此——我们将其称为 <Guide>
组件——它像一个模态框,但由用户可以分页浏览的多个页面组成。
WordPress 本身有一个 <Guide>
组件,在第一次打开块编辑器时会显示欢迎指南。

该指南是一个包含内容的容器,内容被分成单独的页面。换句话说,这正是我们想要的。这意味着我们不必在这个项目中重新发明轮子;我们可以重用这个概念。
让我们这样做。
我们想要实现的目标
在我们开始解决方法之前,让我们先讨论一下最终目标。
该设计满足了插件的需求,该插件是 WordPress 的 GraphQL 服务器。该插件提供了各种通过自定义块编辑的 CPT,而自定义块又通过 模板定义。总共有两个块:一个名为“GraphiQL 客户端”用于输入 GraphQL 查询,另一个名为“持久化查询选项”用于自定义执行行为。
由于创建 GraphQL 查询并非易事,因此我决定将指南组件添加到该 CPT 的编辑器屏幕中。它在文档设置中作为一个名为“欢迎指南”的面板提供。

打开该面板,用户会看到一个链接。该链接将触发模态框。

对于模态框本身,我决定在第一页上显示一个关于如何使用 CPT 的教程视频,然后在后续页面上详细描述 CPT 中可用的所有选项。

我相信这种布局是向用户展示文档的有效方法。它不会妨碍用户操作,但仍然方便地靠近操作区域。当然,我们可以使用不同的设计,甚至可以使用不同的组件将模态框触发器放置在其他位置,而不是重用 <Guide>
,但这已经足够好了。
规划实现
实现包括以下步骤
- 搭建一个新的脚本以注册自定义侧边栏面板
- 仅在我们的自定义文章类型的编辑器中显示自定义侧边栏面板
- 创建指南
- 向指南添加内容
让我们开始吧!
步骤 1:搭建脚本
从 WordPress 5.4 开始,我们可以使用名为 <PluginDocumentSettingPanel>
的组件在编辑器的文档设置中添加一个 面板,如下所示
const { registerPlugin } = wp.plugins;
const { PluginDocumentSettingPanel } = wp.editPost;
const PluginDocumentSettingPanelDemo = () => (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
Custom Panel Contents
</PluginDocumentSettingPanel>
);
registerPlugin( 'plugin-document-setting-panel-demo', {
render: PluginDocumentSettingPanelDemo,
icon: 'palmtree',
} );
如果您熟悉块编辑器并已经知道如何执行此代码,则可以跳过此步骤。我使用块编辑器编程的时间不到三个月,并且使用 React/npm/webpack 对我来说是一个全新的世界——这个插件是我使用它们开发的第一个项目!我发现 Gutenberg 仓库 中的文档并不总是适合像我这样的初学者,有时文档甚至缺失,因此我不得不深入研究源代码以找到答案。
当组件的文档指示使用上面的代码片段时,我不知道接下来该怎么做,因为 <PluginDocumentSettingPanel>
不是一个块,我无法搭建一个新的块或在那里添加代码。此外,我们正在使用 JSX,这意味着我们需要一个 JavaScript 构建步骤 来编译代码。
但是,我找到了 等效的 ES5 代码
var el = wp.element.createElement;
var __ = wp.i18n.__;
var registerPlugin = wp.plugins.registerPlugin;
var PluginDocumentSettingPanel = wp.editPost.PluginDocumentSettingPanel;
function MyDocumentSettingPlugin() {
return el(
PluginDocumentSettingPanel,
{
className: 'my-document-setting-plugin',
title: 'My Panel',
},
__( 'My Document Setting Panel' )
);
}
registerPlugin( 'my-document-setting-plugin', {
render: MyDocumentSettingPlugin
} );
ES5 代码不需要编译,因此我们可以像在 WordPress 中加载任何其他脚本一样加载它。但我不想使用它。我想要完整的、现代的 ESNext 和 JSX 体验。
所以我的想法是这样的:我不能使用 块搭建工具,因为它不是一个块,而且我不知道如何编译脚本(我当然不会自己设置 webpack)。这意味着我被卡住了。
但是等等!块和普通脚本之间的唯一区别在于它们在 WordPress 中的注册方式。块是这样注册的
wp_register_script($blockScriptName, $blockScriptURL, $dependencies, $version);
register_block_type('my-namespace/my-block', [
'editor_script' => $blockScriptName,
]);
而普通脚本是这样注册的
wp_register_script($scriptName, $scriptURL, $dependencies, $version);
wp_enqueue_script($scriptName);
我们可以使用任何块搭建工具来修改内容,然后注册一个普通脚本而不是块,这使我们可以访问 webpack 配置来编译 ESNext 代码。这些可用的工具是
- WP CLI 的 “scaffold” 命令
- Ahmad Awais 的 create-guten-block 包
- 官方的 @wordpress/create-block 包
我选择使用 @wordpress/create-block 包,因为它由开发 Gutenberg 的团队维护。
要搭建块,我们在命令行中执行此操作
npm init @wordpress/block
完成所有信息提示后——包括块的名称、标题和描述——该工具将生成一个 单块插件,其中包含一个入口 PHP 文件,其中包含类似以下的代码
/**
* Registers all block assets so that they can be enqueued through the block editor
* in the corresponding context.
*
* @see https://developer.wordpress.org/block-editor/tutorials/block-tutorial/applying-styles-with-stylesheets/
*/
function my_namespace_my_block_block_init() {
$dir = dirname( __FILE__ );
$script_asset_path = "$dir/build/index.asset.php";
if ( ! file_exists( $script_asset_path ) ) {
throw new Error(
'You need to run `npm start` or `npm run build` for the "my-namespace/my-block" block first.'
);
}
$index_js = 'build/index.js';
$script_asset = require( $script_asset_path );
wp_register_script(
'my-namespace-my-block-block-editor',
plugins_url( $index_js, __FILE__ ),
$script_asset['dependencies'],
$script_asset['version']
);
$editor_css = 'editor.css';
wp_register_style(
'my-namespace-my-block-block-editor',
plugins_url( $editor_css, __FILE__ ),
array(),
filemtime( "$dir/$editor_css" )
);
$style_css = 'style.css';
wp_register_style(
'my-namespace-my-block-block',
plugins_url( $style_css, __FILE__ ),
array(),
filemtime( "$dir/$style_css" )
);
register_block_type( 'my-namespace/my-block', array(
'editor_script' => 'my-namespace-my-block-block-editor',
'editor_style' => 'my-namespace-my-block-block-editor',
'style' => 'my-namespace-my-block-block',
) );
}
add_action( 'init', 'my_namespace_my_block_block_init' );
我们可以将此代码复制到插件中,并进行适当修改,将块转换为普通脚本。(请注意,我还在此过程中删除了 CSS 文件,但如果需要,可以保留它们。)
function my_script_init() {
$dir = dirname( __FILE__ );
$script_asset_path = "$dir/build/index.asset.php";
if ( ! file_exists( $script_asset_path ) ) {
throw new Error(
'You need to run `npm start` or `npm run build` for the "my-script" script first.'
);
}
$index_js = 'build/index.js';
$script_asset = require( $script_asset_path );
wp_register_script(
'my-script',
plugins_url( $index_js, __FILE__ ),
$script_asset['dependencies'],
$script_asset['version']
);
wp_enqueue_script(
'my-script'
);
}
add_action( 'init', 'my_script_init' );
让我们复制 package.json
文件
{
"name": "my-block",
"version": "0.1.0",
"description": "This is my block",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"format:js": "wp-scripts format-js",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"start": "wp-scripts start",
"packages-update": "wp-scripts packages-update"
},
"devDependencies": {
"@wordpress/scripts": "^9.1.0"
}
}
现在,我们可以用上面 ESNext 代码替换文件 src/index.js
的内容来注册 <PluginDocumentSettingPanel>
组件。运行 npm start
(或用于生产环境的 npm run build
)后,代码将被编译到 build/index.js
中。
还有一个问题需要解决:<PluginDocumentSettingPanel>
组件不是静态导入的,而是从 wp.editPost
中获取的,并且由于 wp
是 WordPress 在运行时加载的全局变量,因此此依赖项不存在于 index.asset.php
中(在构建过程中自动生成)。在注册脚本时,我们必须手动向 wp-edit-post
脚本添加依赖项,以确保它在我们脚本之前加载
$dependencies = array_merge(
$script_asset['dependencies'],
[
'wp-edit-post',
]
);
wp_register_script(
'my-script',
plugins_url( $index_js, __FILE__ ),
$dependencies,
$script_asset['version']
);
现在脚本设置已准备就绪!
可以使用 Gutenberg 的持续开发周期更新插件。运行 npm run packages-update
以更新 npm 依赖项(以及由此产生的 webpack 配置,该配置在包 "@wordpress/scripts"
中定义)到其最新支持的版本。
此时,您可能想知道我如何知道在我们的脚本之前向 "wp-edit-post"
脚本添加依赖项。好吧,我不得不深入研究 Gutenberg 的源代码。<PluginDocumentSettingPanel>
的文档有些不足,这完美地说明了 Gutenberg 的文档在某些地方存在不足。
在深入研究代码和浏览文档时,我发现了一些有启发性的内容。例如,有两种方法可以编写我们的脚本:使用 ES5 或 ESNext 语法。ES5 不需要构建过程,并且引用运行时环境中的代码实例,很可能是通过全局 wp
变量。例如,创建图标的代码如下所示
var moreIcon = wp.element.createElement( 'svg' );
ESNext 依靠 webpack 来解析所有依赖项,这使我们能够导入静态组件。例如,创建图标的代码将是
import { more } from '@wordpress/icons';
这几乎适用于所有地方。但是,<PluginDocumentSettingPanel>
组件并非如此,它 引用了 ESNext 的运行时环境
const { PluginDocumentSettingPanel } = wp.editPost;
这就是为什么我们必须向“wp-edit-post”脚本添加依赖项的原因。wp.editPost
变量是在那里定义的。
如果可以直接导入<PluginDocumentSettingPanel>
,那么对“wp-edit-post”的依赖将由块编辑器通过Dependency Extraction Webpack Plugin自动处理。此插件通过创建一个包含运行时环境脚本所有依赖项的index.asset.php
文件,构建了从静态到运行时的桥梁,这些依赖项是通过将包名称中的"@wordpress/"
替换为"wp-"
获得的。因此,"@wordpress/edit-post"
包变成了"wp-edit-post"
运行时脚本。这就是我如何确定要添加哪个脚本作为依赖项。
步骤 2:在所有其他自定义文章类型上将自定义侧边栏面板列入黑名单
该面板将显示特定自定义文章类型的文档,因此它必须仅注册到该自定义文章类型。这意味着我们需要将其列入黑名单,使其不会出现在任何其他文章类型上。
Ryan Welcher(创建<PluginDocumentSettingPanel>
组件的人)在注册面板时描述了此过程
const { registerPlugin } = wp.plugins;
const { PluginDocumentSettingPanel } = wp.editPost
const { withSelect } = wp.data;
const MyCustomSideBarPanel = ( { postType } ) => {
if ( 'post-type-name' !== postType ) {
return null;
}
return(
<PluginDocumentSettingPanel
name="my-custom-panel"
title="My Custom Panel"
>
Hello, World!
</PluginDocumentSettingPanel>
);
}
const CustomSideBarPanelwithSelect = withSelect( select => {
return {
postType: select( 'core/editor' ).getCurrentPostType(),
};
} )( MyCustomSideBarPanel);
registerPlugin( 'my-custom-panel', { render: CustomSideBarPanelwithSelect } );
他还建议了一种替代方案,使用useSelect
而不是withSelect
。
也就是说,我并不完全相信这种解决方案,因为即使不需要,JavaScript 文件也必须加载,这会迫使网站承受性能损失。难道不注册 JavaScript 文件比运行 JavaScript 仅仅是为了禁用 JavaScript 更有意义吗?
我创建了一个 PHP 解决方案。我承认它感觉有点像黑客行为,但它运行良好。首先,我们找出哪个文章类型与正在创建或编辑的对象相关联。
function get_editing_post_type(): ?string
{
if (!is_admin()) {
return null;
}
global $pagenow;
$typenow = '';
if ( 'post-new.php' === $pagenow ) {
if ( isset( $_REQUEST['post_type'] ) && post_type_exists( $_REQUEST['post_type'] ) ) {
$typenow = $_REQUEST['post_type'];
};
} elseif ( 'post.php' === $pagenow ) {
if ( isset( $_GET['post'] ) && isset( $_POST['post_ID'] ) && (int) $_GET['post'] !== (int) $_POST['post_ID'] ) {
// Do nothing
} elseif ( isset( $_GET['post'] ) ) {
$post_id = (int) $_GET['post'];
} elseif ( isset( $_POST['post_ID'] ) ) {
$post_id = (int) $_POST['post_ID'];
}
if ( $post_id ) {
$post = get_post( $post_id );
$typenow = $post->post_type;
}
}
return $typenow;
}
然后,我们仅在它与我们的自定义文章类型匹配时注册脚本。
add_action('init', 'maybe_register_script');
function maybe_register_script()
{
// Check if this is the intended custom post type
if (get_editing_post_type() != 'my-custom-post-type') {
return;
}
// Only then register the block
wp_register_script(...);
wp_enqueue_script(...);
}
查看这篇文章以更深入地了解其工作原理。
步骤 3:创建自定义指南
我根据 WordPress 的<Guide>
组件设计了插件指南的功能。一开始我没有意识到我会这样做,所以这就是我如何弄清楚的。
- 搜索源代码以查看它是如何在其中完成的。
- 在Gutenberg 的 Storybook中浏览所有可用组件的目录。
首先,我从块编辑器模态复制内容并进行了基本的搜索。结果将我引导至此文件。从那里我发现该组件称为<Guide>
,可以简单地复制粘贴其代码到我的插件中作为我自己的指南的基础。
然后我查找了组件的文档。我浏览了@wordpress/components包(正如你可能猜到的,组件是在这里实现的)并找到了组件的 README 文件。这给了我实现我自己的自定义指南组件所需的所有信息。
我还浏览了Gutenberg 的 Storybook中所有可用组件的目录(实际上显示了这些组件可以在 WordPress 上下文之外使用)。点击所有组件,我最终发现了<Guide>
。Storybook 为几个示例(或故事)提供了源代码。这是一个方便的资源,可以帮助理解如何通过 props 自定义组件。
此时,我知道<Guide>
将成为我组件的坚实基础。不过,缺少一个元素:如何通过点击触发指南。我不得不绞尽脑汁想出这个!
这是一个带有监听器的按钮,点击时会打开模态。
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import MyGuide from './guide';
const MyGuideWithButton = ( props ) => {
const [ isOpen, setOpen ] = useState( false );
return (
<>
<Button onClick={ () => setOpen( true ) }>
{ __('Open Guide: “Creating Persisted Queries”') }
</Button>
{ isOpen && (
<MyGuide
{ ...props }
onFinish={ () => setOpen( false ) }
/>
) }
</>
);
};
export default MyGuideWithButton;
即使块编辑器试图隐藏它,我们仍在 React 中操作。到目前为止,我们一直在处理 JSX 和组件。但现在我们需要useState
hook,它是 React 特有的。
我想说,如果你想掌握 WordPress 块编辑器,就需要对 React 有很好的理解。这是无法避免的。
步骤 4:向指南添加内容
我们快到了!让我们创建<Guide>
组件,每个内容页面都包含一个<GuidePage>
组件。
内容可以使用 HTML,包含其他组件等等。在本例中,我只是使用 HTML 为我的自定义文章类型添加了三个<GuidePage>
实例。第一页包含视频教程,接下来的两页包含详细说明。
import { Guide, GuidePage } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
const MyGuide = ( props ) => {
return (
<Guide { ...props } >
<GuidePage>
<video width="640" height="400" controls>
<source src="https://d1c2lqfn9an7pb.cloudfront.net/presentations/graphql-api/videos/graphql-api-creating-persisted-query.mov" type="video/mp4" />
{ __('Your browser does not support the video tag.') }
</video>
// etc.
</GuidePage>
<GuidePage>
// ...
</GuidePage>
<GuidePage>
// ...
</GuidePage>
</Guide>
)
}
export default MyGuide;

不错!不过,也有一些问题。
- 我无法将视频嵌入到
<Guide>
中,因为点击播放按钮会关闭指南。我假设这是因为<iframe>
超出了指南的边界。我最终将视频文件上传到 S3 并使用<video>
提供服务。 - 指南中的页面过渡不够流畅。块编辑器的模态看起来不错,因为所有页面都具有相似的高度,但此处的过渡非常突然。
- 按钮上的悬停效果可以改进。希望 Gutenberg 团队需要为他们自己的目的修复这个问题,因为我的 CSS 不在那里。并不是我的技能不好;它们根本不存在。
但我可以忍受这些问题。在功能方面,我已经实现了指南需要执行的操作。
额外内容:独立打开文档
对于我们的<Guide>
,我们直接使用 HTML 创建了每个<GuidePage>
组件的内容。但是,如果此 HTML 代码改为通过一个独立的组件添加,则可以重复用于其他用户交互。
例如,组件<CacheControlDescription>
显示有关 HTTP 缓存的描述。
const CacheControlDescription = () => {
return (
<p>The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or "no-store" if the max-age is 0</p>
)
}
export default CacheControlDescription;
此组件可以像我们之前那样添加到<GuidePage>
中,也可以添加到<Modal>
组件中。
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import CacheControlDescription from './cache-control-desc';
const CacheControlModalWithButton = ( props ) => {
const [ isOpen, setOpen ] = useState( false );
return (
<>
<Button
icon="editor-help"
onClick={ () => setOpen( true ) }
/>
{ isOpen && (
<Modal
{ ...props }
onRequestClose={ () => setOpen( false ) }
>
<CacheControlDescription />
</Modal>
) }
</>
);
};
export default CacheControlModalWithButton;
为了提供良好的用户体验,我们可以在用户与块交互时才提供显示文档。为此,我们根据isSelected
的值显示或隐藏按钮。
import { __ } from '@wordpress/i18n';
import CacheControlModalWithButton from './modal-with-btn';
const CacheControlHeader = ( props ) => {
const { isSelected } = props;
return (
<>
{ __('Cache-Control max-age') }
{ isSelected && (
<CacheControlModalWithButton />
) }
</>
);
}
export default CacheControlHeader;
最后,<CacheControlHeader>
组件被添加到相应的控件中。

太棒啦 🎉
WordPress 块编辑器是一款相当棒的软件!我能够用它完成一些如果没有它就无法完成的事情。为用户提供文档可能不是最闪亮的示例或用例,但它是一个非常实用的用例,并且与许多其他插件相关。想在你的插件中使用它吗?尽管来吧!
查看此 Cloudways 教程以进一步了解如何设置自定义页面模板。