WordPress 块过滤器速成课程

Avatar of Dmitry Mayorov
Dmitry Mayorov 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

WordPress 中的块 非常棒。将一些块拖放到页面上,按照您喜欢的样式排列,您就可以轻松创建一个非常棒的登录页面。但是,如果 WordPress 中的默认块需要进行一些调整怎么办?例如,如果我们能够删除封面块设置中的对齐选项怎么办?或者如何控制按钮块的大小?

在扩展 WordPress 中核心块的功能方面,有很多选择。我们可以在编辑器中向块添加自定义 CSS 类,添加自定义样式,或创建 块变体。但即使这些可能不足以满足您的需求,您也可能需要过滤核心块以添加或删除功能,或者从头开始构建一个全新的块。

我将向您展示如何使用过滤器扩展核心块,并简要介绍何时最好构建自定义块而不是扩展核心块。

关于这些示例的快速说明

在我们深入探讨之前,我想指出本文中的代码片段是故意脱离上下文的,目的是专注于过滤器而不是构建工具和文件结构。如果我包含了过滤器的完整代码,本文将难以理解。话虽如此,我理解对于刚开始学习的人来说,不清楚将代码片段放在哪里或如何运行构建脚本并使整个过程正常工作。

为了方便您,我在我的 GitHub 上提供了一个包含本文示例的 WordPress 插件。欢迎您 下载它 并探索文件结构、依赖项和构建脚本。有一个 README 可以帮助您入门。

简而言之的块过滤器

过滤器的概念对 WordPress 来说并不新鲜。我们大多数人都熟悉 PHP 中的 add_filter() 函数。它允许开发人员 使用钩子 修改各种类型的数据。

一个简单的 PHP 过滤器示例可能如下所示

function filter_post_title( $title ){
  return '<strong>' . $title . '</strong>';
};

add_filter( 'the_title',  'filter_post_title' );

在此代码片段中,我们创建一个函数,该函数接收表示帖子标题的字符串,然后将其包装在 <strong> 标记中并返回修改后的标题。然后,我们使用 add_filter() 告诉 WordPress 在帖子标题上使用该函数。

JavaScript 过滤器的工作方式类似。有一个名为 addFilter() 的 JavaScript 函数位于 wp.hooks 包中,其工作方式几乎与它的 PHP 同类函数相同。在最简单的形式中,JavaScript 过滤器看起来像这样

function filterSomething(something) {
  // Code for modifying something goes here.
  return something;
}

wp.hooks.addFilter( 'hookName', 'namespace', filterSomething );

看起来非常相似,对吧?一个值得注意的区别是 addFilter() 将命名空间作为第二个参数。 根据 WordPress 手册,“命名空间以 vendor/plugin/function 的形式唯一标识回调。”但是,手册中的示例遵循不同的模式:plugin/what-filter-doesplugin/component-name/what-filter-does。我通常遵循后者,因为它使整个项目中的句柄保持唯一。

使 JavaScript 过滤器难以理解和使用的是它们可以过滤内容的不同性质。一些过滤器过滤字符串,一些过滤器过滤 JavaScript 对象,而另一些过滤器过滤 React 组件,并且需要理解 高阶组件 的概念。

最重要的是,您很可能需要使用 JSX,这意味着您不能只将代码放入您的主题或插件中并期望它能够工作。您需要将其转换为浏览器理解的普通 JavaScript。所有这些在开始时都可能令人望而生畏,尤其是在您来自 PHP 背景并且对 ES6、JSX 和 React 的了解有限的情况下。

但是不要害怕!我们有两个示例涵盖了块过滤器的基础知识,以帮助您掌握这个概念,并对在 WordPress 中使用 JavaScript 过滤器感到舒适。作为提醒,如果您不熟悉为块编辑器编写此代码,请 探索包含本文示例的插件

事不宜迟,让我们看看第一个示例。

删除封面块的对齐选项

我们将过滤核心封面块并从其块设置中删除对齐选项。这在仅将封面块用作页面英雄或某种横幅且不需要左对齐或右对齐的项目中可能很有用。

我们将使用 blocks.registerBlockType 过滤器。它接收块的设置及其名称,并且必须返回一个经过过滤的设置对象。过滤设置允许我们更新包含可用对齐数组的 supports 对象。让我们一步一步地进行。

我们将首先添加一个过滤器,该过滤器仅将设置和块的名称记录到控制台,以查看我们正在处理什么

const { addFilter } = wp.hooks;

function filterCoverBlockAlignments(settings, name) {
  console.log({ settings, name });
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/cover-block/alignment-settings',
  filterCoverBlockAlignments,
);

让我们分解一下。第一行是 wp.hooks 对象的基本 解构。它允许我们在文件的其余部分编写 addFilter(),而不是 wp.hooks.addFilter()。在这种情况下,这可能看起来是多余的,但在同一文件中使用多个过滤器时很有用(我们将在下一个示例中介绍)。

接下来,我们定义了执行过滤的 filterCoverBlockAlignments() 函数。目前,它仅将设置对象和块的名称记录到控制台,并按原样返回设置。

所有过滤器函数都接收数据,并且必须返回过滤后的数据。否则,编辑器将崩溃。

最后,我们使用 addFilter() 函数启动了过滤器。我们为它提供了我们将要使用的钩子的名称、过滤器命名空间以及执行过滤的函数。

如果我们一切操作正确,我们应该在控制台中看到很多消息。但请注意,并非所有消息都与封面块有关。

这是正确的,因为过滤器应用于所有块,而不是我们想要的特定块。为了解决这个问题,我们需要确保我们仅对 core/cover 块应用过滤器

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    console.log({ settings, name });
  }
  return settings;
}

有了这个,我们现在应该在控制台中看到类似这样的内容

如果您在页面上看到的日志语句多于封面块,请不要担心。我还没有弄清楚为什么会这样。如果您知道原因,请在评论中分享!

接下来是有趣的部分:实际过滤。如果您之前从头开始构建过块,那么您就会知道对齐选项是使用 支持 API 定义的。让我快速提醒您它是如何工作的——我们可以将其设置为 true 以允许所有对齐,如下所示

supports: {
  align: true
}

…或提供要支持的对齐数组。以下代码片段与上面的代码片段执行相同操作

supports: {
  align: [ 'left', 'right', 'center', 'wide', 'full' ]
}

现在让我们仔细看看我们从其中一条控制台消息中获得的 settings 对象,看看我们正在处理什么

我们需要做的就是在 supports 属性内用 align: ['full'] 替换 align: true。以下是我们的操作方法

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    return assign({}, settings, {
      supports: merge(settings.supports, {
        align: ['full'],
      }),
    });
  }
  return settings;
}

我想在这里暂停一下,提请您注意 assignmerge lodash 方法。我们使用它们来创建和返回一个全新的对象,并确保原始 settings 对象保持不变。如果我们执行以下操作,过滤器仍然可以工作

/* 👎 WRONG APPROACH! DO NOT COPY & PASTE! */
settings.supports.align = ['full'];
return settings;

…但这是一种对象变异,被认为是不好的做法,应避免,除非您知道自己在做什么。Zell Liew 在 A List Apart 上讨论了 为什么变异可能很可怕

回到我们的示例,现在块工具栏中应该只有一个对齐选项

我删除了“居中”对齐选项,因为对齐工具栏允许您切换对齐的“开”和“关”。这意味着封面块现在具有默认状态和“全宽”状态。

以下是完整代码片段

const { addFilter } = wp.hooks;
const { assign, merge } = lodash;

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    return assign({}, settings, {
      supports: merge(settings.supports, {
        align: ['full'],
      }),
    });
}
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/cover-block/alignment-settings',
  filterCoverBlockAlignments,
);

这根本不难,对吧?您现在已经掌握了块过滤器工作原理的基本知识。让我们更上一层楼,看看一个稍微高级一点的例子。

向按钮块添加大小控制

现在让我们向核心按钮块添加一个大小控制。它会稍微复杂一些,因为我们需要使几个过滤器协同工作。计划是添加一个控件,允许用户为按钮选择三种大小:常规和大。

目标是启动那个新的“大小设置”部分。

这看起来可能很复杂,但是一旦我们将其分解,您就会发现它实际上非常简单。

1. 为按钮块添加大小属性

首先,我们需要添加一个额外的属性来存储按钮的大小。我们将使用前面示例中已经熟悉的 `blocks.registerBlockType` 过滤器。

/**
 * Add Size attribute to Button block
 *
 * @param  {Object} settings Original block settings
 * @param  {string} name     Block name
 * @return {Object}          Filtered block settings
 */
function addAttributes(settings, name) {
  if (name === 'core/button') {
    return assign({}, settings, {
      attributes: merge(settings.attributes, {
        size: {
          type: 'string',
          default: '',
        },
      }),
    });
  }
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/button-block/add-attributes',
  addAttributes,
);

这里我们所做的事情与之前有所不同,我们过滤的是 `attributes` 而不是 `supports` 对象。仅此片段代码并不会产生太大效果,您在编辑器中也不会注意到任何区别,但为大小添加属性对于整个功能的实现至关重要。

2. 为按钮块添加大小控制

我们正在使用一个新的过滤器,` editor.BlockEdit`。它允许我们修改检查器控件面板(即块编辑器右侧的设置面板)。

/**
 * Add Size control to Button block
 */
const addInspectorControl = createHigherOrderComponent((BlockEdit) => {
  return (props) => {
    const {
      attributes: { size },
      setAttributes,
      name,
    } = props;
    if (name !== 'core/button') {
      return <BlockEdit {...props} />;
    }
    return (
      <Fragment>
        <BlockEdit {...props} />
        <InspectorControls>
          <PanelBody
            title={__('Size settings', 'intro-to-filters')}
            initialOpen={false}
          >
            <SelectControl
              label={__('Size', 'intro-to-filters')}
              value={size}
              options={[
                {
                  label: __('Regular', 'intro-to-filters'),
                  value: 'regular',
                },
                {
                  label: __('Small', 'intro-to-filters'),
                  value: 'small'
                },
                {
                  label: __('Large', 'intro-to-filters'),
                  value: 'large'
                },
              ]}
              onChange={(value) => {
                setAttributes({ size: value });
              }}
            />
          </PanelBody>
      </InspectorControls>
      </Fragment>
    );
  };
}, 'withInspectorControl');

addFilter(
  'editor.BlockEdit',
  'intro-to-filters/button-block/add-inspector-controls',
  addInspectorControl,
);

这看起来可能很多,但我们将对其进行分解,看看它实际上有多简单。

您可能注意到的第一件事是 ` createHigherOrderComponent` 结构。与本示例中的其他过滤器不同,` editor.BlockEdit` 会接收一个 *组件* 并必须返回一个 *组件*。这就是为什么我们需要使用源自 React 的 高阶组件 模式。

在最纯粹的形式中,添加控件的过滤器看起来像这样

const addInspectorControl = createHigherOrderComponent((BlockEdit) => {
  return (props) => {
    // Logic happens here.
    return <BlockEdit {...props} />;
  };
}, 'withInspectorControl');

这将不会做任何事情,只会允许您在控制台中检查 `` 组件及其 `props`。希望该结构本身现在有意义了,我们可以继续分解过滤器。

下一部分是对 props 进行解构

const {
  attributes: { size },
  setAttributes,
  name,
} = props;

这样做是为了我们能够在过滤器的范围内使用 `name`、`setAttributes` 和 `size`,其中

  • `size` 是我们在步骤 1 中添加的块的属性。
  • `setAttributes` 是一个函数,它允许我们更新块的属性值。
  • `name` 是块的名称,在我们的例子中是 `core/button`。

接下来,我们避免无意中向其他块添加控件

if (name !== 'core/button') {
  return <BlockEdit {...props} />;
}

如果我们确实正在处理按钮块,我们将设置面板包装在一个 `` 中(一个不带包装元素即可渲染其子元素的组件)并添加一个额外的控件来选择按钮大小

return (
  <Fragment>
    <BlockEdit {...props} />
    {/* Additional controls go here */}
  </Fragment>
);

最后,创建额外的控件如下所示

<InspectorControls>
  <PanelBody title={__('Size settings', 'intro-to-filters')} initialOpen={false}>
    <SelectControl
      label={__('Size', 'intro-to-filters')}
      value={size}
      options={[
        { label: __('Regular', 'intro-to-filters'), value: 'regular' },
        { label: __('Small', 'intro-to-filters'), value: 'small' },
        { label: __('Large', 'intro-to-filters'), value: 'large' },
      ]}
      onChange={(value) => {
        setAttributes({ size: value });
      }}
    />
  </PanelBody>
</InspectorControls>

同样,如果您之前构建过块,您可能已经熟悉了这部分内容。如果不是,我建议您学习 WordPress 附带的 组件库

此时,我们应该在每个按钮块的检查器控件中看到一个额外的部分

我们也可以保存大小,但这不会反映在编辑器或前端。让我们解决这个问题。

3. 在编辑器中为块添加大小类

顾名思义,此步骤的计划是为按钮块添加一个 CSS 类,以便在编辑器本身中反映所选大小。

我们将使用 ` editor.BlockListBlock` 过滤器。它类似于 `editor.BlockEdit`,因为它接收组件并且必须返回组件;但它不是过滤块检查器面板,而是过滤在编辑器中显示的块组件。

import classnames from 'classnames';
const { addFilter } = wp.hooks;
const { createHigherOrderComponent } = wp.compose;

/**
 * Add size class to the block in the editor
 */
const addSizeClass = createHigherOrderComponent((BlockListBlock) => {
  return (props) => {
    const {
      attributes: { size },
      className,
      name,
    } = props;

    if (name !== 'core/button') {
      return <BlockListBlock {...props} />;
    }

    return (
      <BlockListBlock
        {...props}
        className={classnames(className, size ? `has-size-${size}` : '')}
      />
    );
  };
}, 'withClientIdClassName');

addFilter(
   'editor.BlockListBlock',
   'intro-to-filters/button-block/add-editor-class',
   addSizeClass
);

您可能已经注意到类似的结构

  1. 我们从 `props` 中提取 `size`、`className` 和 `name` 变量。
  2. 接下来,我们检查是否正在使用 `core/button` 块,如果不是,则返回未修改的 ``。
  3. 然后我们根据所选按钮大小向块添加一个类。

我想在这里暂停一下,因为乍一看它可能看起来很混乱

className={classnames(className, size ? `has-size-${size}` : '')}

我在这里使用 classnames 实用程序,这不是必需的——我只是觉得使用它比手动连接更简洁。它使我无需担心忘记在类前面添加空格或处理双空格。

4. 在前端为块添加大小类

到目前为止,我们所做的一切都与块编辑器视图相关,这有点像我们可能在前端看到的预览。如果我们更改按钮大小,保存帖子并在前端检查按钮标记,请注意按钮类未应用于块。

要解决此问题,我们需要确保我们确实保存了更改并将类添加到前端的块中。我们使用 ` blocks.getSaveContent.extraProps` 过滤器,它挂接到块的 `save()` 函数并允许我们修改保存的属性。此过滤器接收块 props、块类型和块属性,并且必须返回修改后的块 props。

import classnames from 'classnames';
const { assign } = lodash;
const { addFilter } = wp.hooks;

/**
 * Add size class to the block on the front end
 *
 * @param  {Object} props      Additional props applied to save element.
 * @param  {Object} block      Block type.
 * @param  {Object} attributes Current block attributes.
 * @return {Object}            Filtered props applied to save element.
 */
function addSizeClassFrontEnd(props, block, attributes) {
  if (block.name !== 'core/button') {
    return props;
  }

  const { className } = props;
  const { size } = attributes;

  return assign({}, props, {
    className: classnames(className, size ? `has-size-${size}` : ''),
  });
}

addFilter(
  'blocks.getSaveContent.extraProps',
  'intro-to-filters/button-block/add-front-end-class',
  addSizeClassFrontEnd,
);

在上边的代码片段中,我们做了三件事

  1. 检查我们是否正在使用 `core/button` 块,如果不是,则快速返回。
  2. 分别从 `props` 和 `attributes` 对象中提取 `className` 和 `size` 变量。
  3. 创建一个新的 `props` 对象,其中包含一个更新的 `className` 属性,如果需要,该属性包括一个大小类。

以下是我们应该在标记中看到的,包括我们的尺寸类

<div class="wp-block-button has-size-large">
  <a class="wp-block-button__link" href="#">Click Me</a>
</div>

5. 为自定义按钮大小添加 CSS

在完成之前还有一件小事!我们的想法是确保大按钮和小按钮具有相应的 CSS 样式。

以下是我想到的样式

.wp-block-button.has-size-large .wp-block-button__link {
  padding: 1.5rem 3rem;
}
.wp-block-button.has-size-small .wp-block-button__link {
  padding: 0.25rem 1rem;
}

如果您正在构建自定义主题,则可以在主题的样式表中包含这些前端样式。我为默认的 Twenty Twenty One 主题创建了一个插件,因此,在我的情况下,我必须创建一个单独的样式表并使用 `wp_enqueue_style()` 包含它。如果您是在 `functions.php` 中管理函数,也可以直接在其中进行操作。

function frontend_assets() {
  wp_enqueue_style(
    'intro-to-block-filters-frontend-style',
    plugin_dir_url( __FILE__ ) . 'assets/frontend.css',
    [],
    '0.1.0'
  );
}
add_action( 'wp_enqueue_scripts', 'frontend_assets' );

与前端类似,我们需要确保按钮在编辑器中也正确设置了样式。我们可以使用 `enqueue_block_editor_assets` 操作包含相同的样式

function editor_assets() {
  wp_enqueue_style(
    'intro-to-block-filters-editor-style',
    plugin_dir_url( __FILE__ ) . 'assets/editor.css',
    [],
    '0.1.0'
  );
}
add_action( 'enqueue_block_editor_assets', 'editor_assets' );

现在我们应该在前端和编辑器中都有大按钮和小按钮的样式了!

如前所述,这些示例在我的 WordPress 插件 中可用,我专门为本文创建了它。因此,如果您想了解所有这些部分如何协同工作,请在 GitHub 上下载它并进行修改。如果某些内容不清楚,请随时在评论中提问。

使用过滤器还是创建新块?

在不知道上下文的情况下,这个问题很难回答。但我可以提供一个提示。

您是否曾经见过这样的错误?

它通常发生在页面上块的标记与块的 `save()` 函数生成的标记不同时。我的意思是,在使用过滤器修改块的标记时,很容易触发此错误。

因此,如果您需要大幅更改块的标记(不仅仅是添加一个类),我建议您编写一个自定义块而不是过滤现有的块。也就是说,除非您不介意保持编辑器的标记一致,并且只更改前端标记。在这种情况下,您可以使用 PHP 过滤器。

说到这里……

额外提示:`render_block()`

本文如果不提及 ` render_block` 钩子就不完整了。它在渲染块标记 *之前* 对其进行过滤。当您需要更新块的标记(不仅仅是添加新类)时,它会派上用场。

这种方法的最大好处是不会在编辑器中导致任何验证错误。也就是说,缺点是它只在前端有效。如果我要使用这种方法重写按钮大小示例,我首先需要删除我们在第四步中编写的代码,并添加以下内容

/**
 * Add button size class.
 *
 * @param  string $block_content Block content to be rendered.
 * @param  array  $block         Block attributes.
 * @return string
 */
function add_button_size_class( $block_content = '', $block = [] ) {
  if ( isset( $block['blockName'] ) && 'core/button' === $block['blockName'] ) {
    $defaults = ['size' => 'regular'];
    $args = wp_parse_args( $block['attrs'], $defaults );

    $html = str_replace(
      '<div class="wp-block-button',
      '<div class="wp-block-button has-size-' . esc_attr( $args['size']) . ' ',
      $block_content
    );

    return $html;
}
  return $block_content;
}

add_filter( 'render_block', 'add_button_size_class', 10, 2 );

这不是最简洁的方法,因为我们正在使用str_replace()注入 CSS 类,但这有时是唯一的选择。一个经典的例子可能是处理第三方区块,我们需要在其周围添加一个带有类的<div>用于样式。

总结

WordPress 区块过滤器功能强大。我喜欢它允许你禁用许多未使用的区块选项,就像我们在第一个示例中对封面区块所做的那样。这可以减少你需要编写的 CSS 量,从而意味着更精简的样式表和更少的维护,以及使用区块设置的任何人的认知开销更少。

但正如我之前提到的,使用区块过滤器进行大量修改可能会变得棘手,因为你需要牢记区块验证。

也就是说,如果我需要,我通常会使用区块过滤器

  • 禁用某些区块功能,
  • 向区块添加选项,并且无法/不想使用自定义样式来执行此操作(并且该选项不得修改区块的标记,而只能添加/删除自定义类),或者
  • 仅在前端修改标记(使用 PHP 过滤器)。

当核心区块需要在前端和编辑器中进行大量标记调整时,我最终也会编写自定义区块。

如果你使用过区块过滤器,并且有其他想法、问题或评论,请告诉我!

资源