深入理解 WordPress `save_post` 钩子在 Gutenberg 中的源码:如何处理区块数据的保存流程。

大家好,我是老码农,今天咱们聊聊 WordPress 里一个非常关键的钩子:save_post,特别是它在古腾堡编辑器(Gutenberg)中是如何玩转区块数据的保存流程的。这玩意儿,说白了,就是把你在编辑器里辛辛苦苦堆砌的各种区块,变成数据库里能识别、能还原的数据。

咱们先从最基础的概念入手,然后一步步深入源码,最后再来点实际案例,让大家彻底明白这其中的奥秘。准备好了吗?Let’s roll!

第一幕:钩子是个啥?为啥要用它?

想象一下,WordPress 是一个大舞台,各种事件都在这里上演。save_post 钩子就像一个侦听器,它时刻监听着“文章要保存了!”这个事件。一旦这个事件发生,它就会触发你预先注册好的函数,让你有机会在文章真正保存到数据库之前,做一些手脚,比如修改文章内容、更新自定义字段等等。

为什么要用钩子呢?因为 WordPress 本身的代码已经写死了,但我们总有各种各样的定制需求。钩子就提供了一个“官方”的扩展点,让我们可以在不修改 WordPress 核心代码的前提下,实现各种各样的功能。这就像盖房子,主体结构已经完成了,但你可以通过预留的接口,往里面添加各种家具、电器,让房子更符合你的需求。

第二幕:save_post 钩子的基本用法

save_post 钩子接受两个参数:

  • $post_id: 要保存的文章的 ID。
  • $post: WP_Post 对象,包含了文章的所有信息。

使用 add_action() 函数可以注册你的自定义函数,让它在 save_post 事件发生时被调用。

add_action( 'save_post', 'my_custom_save_post' );

function my_custom_save_post( $post_id, $post ) {
  // 检查是否是自动保存
  if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
    return;
  }

  // 检查用户是否有权限编辑文章
  if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
  }

  // 检查文章类型是否是 post
  if ( 'post' !== $post->post_type ) {
    return;
  }

  // 这里写你的自定义逻辑,比如更新自定义字段
  update_post_meta( $post_id, '_my_custom_field', $_POST['my_custom_field'] );
}

这段代码就是一个最基本的 save_post 钩子的用法。它做了以下几件事:

  1. 注册了一个名为 my_custom_save_post 的函数,让它在 save_post 事件发生时被调用。
  2. 在函数内部,首先进行了一系列安全检查,防止恶意代码的执行。
  3. 最后,更新了一个名为 _my_custom_field 的自定义字段,它的值来自于 $_POST 数组。

第三幕:古腾堡区块数据的存储方式

古腾堡编辑器将文章内容分解为一个个独立的区块,每个区块都有自己的属性和内容。这些区块数据最终会被存储到文章的 post_content 字段中,以特殊的格式进行标记。

具体来说,古腾堡使用了一种名为“序列化区块”的格式来存储区块数据。这种格式使用 HTML 注释来标记区块的开始和结束,并在注释中包含区块的类型和属性。

一个典型的序列化区块的例子如下:

<!-- wp:paragraph -->
<p>这是一个段落区块。</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":123,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="image.jpg" alt="" class="wp-image-123"/></figure>
<!-- /wp:image -->

可以看到,每个区块都用 <!-- wp:block-name --><!-- /wp:block-name --> 包裹,其中 block-name 是区块的名称。有些区块还会在起始注释中包含 JSON 格式的属性,比如上面的 image 区块。

第四幕:save_post 如何处理区块数据?源码解读

现在,我们终于要进入正题了,看看 save_post 钩子是如何处理这些区块数据的。

WordPress 核心代码中,负责处理 save_post 钩子的函数位于 wp-includes/post.php 文件中,名为 wp_insert_post()。这个函数非常庞大,包含了文章保存的所有逻辑,包括处理区块数据。

为了简化分析,我们只关注与区块数据相关的部分。

  1. wp_insert_post() 函数接收到文章数据后,会首先对 post_content 字段进行处理。

    这个处理过程包括:

    • 解析序列化区块:post_content 中的序列化区块解析成一个 PHP 数组,方便后续处理。
    • 过滤区块内容: 对每个区块的内容进行过滤,防止 XSS 攻击和其他安全问题。
    • 更新自定义字段: 有些区块会将数据存储到自定义字段中,wp_insert_post() 函数会负责更新这些自定义字段。
    • 重新序列化区块: 经过一系列处理后,wp_insert_post() 函数会将区块数据重新序列化成 HTML 注释,并更新 post_content 字段。
  2. parse_blocks() 函数:解析序列化区块

    parse_blocks() 函数位于 wp-includes/blocks.php 文件中,是负责解析序列化区块的核心函数。

    它会将 post_content 中的字符串解析成一个区块数组,每个数组元素代表一个区块,包含了区块的名称、属性和内容。

    function parse_blocks( $content ) {
        if ( ! is_string( $content ) ) {
            return array();
        }
    
        $blocks = array();
        $parser = new WP_Block_Parser();
        $results = $parser->parse( $content );
    
        if ( ! $results ) {
            return array( array(
                'blockName' => null,
                'attrs'     => array(),
                'innerBlocks' => array(),
                'innerHTML' => $content,
                'innerContent' => array( $content ),
            ) );
        }
    
        foreach ( $results as $result ) {
            $blocks[] = $result->to_array();
        }
    
        return $blocks;
    }

    这个函数首先创建了一个 WP_Block_Parser 对象,然后调用 parse() 方法来解析 post_contentWP_Block_Parser 类负责识别 HTML 注释中的区块标记,并提取区块的名称和属性。

  3. serialize_block() 函数:重新序列化区块

    parse_blocks() 函数相反,serialize_block() 函数负责将区块数组重新序列化成 HTML 注释。

    这个函数会将区块的名称和属性转换成 JSON 格式,并插入到 HTML 注释中。

    function serialize_block( $block ) {
        $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null;
        $attrs      = isset( $block['attrs'] )     ? $block['attrs']     : array();
        $inner_html = isset( $block['innerHTML'] ) ? $block['innerHTML'] : '';
    
        $output = '';
    
        if ( $block_name ) {
            $output .= "<!-- wp:" . $block_name . " " . wp_json_encode( $attrs ) . " -->n";
            $output .= $inner_html . "n";
            $output .= "<!-- /wp:" . $block_name . " -->n";
        } else {
            $output .= $inner_html . "n";
        }
    
        return $output;
    }

    这个函数会根据区块的名称和属性,生成相应的 HTML 注释,并将区块的内容插入到注释之间。

第五幕:自定义区块与 save_post

如果你开发了自己的自定义区块,那么你需要使用 save_post 钩子来处理这些区块的数据。

通常情况下,你需要做以下几件事:

  1. 注册自定义字段: 如果你的区块需要存储一些额外的数据,你需要注册一些自定义字段来存储这些数据。
  2. 编写 save 函数: 在你的区块定义中,你需要编写一个 save 函数,这个函数会在文章保存时被调用。
  3. 使用 save_post 钩子更新自定义字段:save_post 钩子的回调函数中,你需要读取区块的数据,并将它们存储到自定义字段中。

举个例子,假设你开发了一个名为 my-plugin/custom-block 的自定义区块,它有一个名为 my_custom_text 的属性,你想将这个属性的值存储到一个名为 _my_custom_block_text 的自定义字段中。

首先,你需要注册这个自定义字段:

function my_plugin_register_meta() {
  register_post_meta( 'post', '_my_custom_block_text', array(
    'show_in_rest' => true,
    'single'       => true,
    'type'         => 'string',
  ) );
}
add_action( 'init', 'my_plugin_register_meta' );

然后,在你的区块定义中,你需要编写 save 函数:

// JavaScript (React) 代码
const { registerBlockType } = wp.blocks;
const { TextControl } = wp.components;

registerBlockType( 'my-plugin/custom-block', {
  title: 'Custom Block',
  icon: 'smiley',
  category: 'common',
  attributes: {
    my_custom_text: {
      type: 'string',
      default: '',
    },
  },
  edit: ( { attributes, setAttributes } ) => {
    const { my_custom_text } = attributes;

    return (
      <TextControl
        label="Custom Text"
        value={ my_custom_text }
        onChange={ ( newText ) => setAttributes( { my_custom_text: newText } ) }
      />
    );
  },
  save: ( { attributes } ) => {
    // 注意这里只是返回一个占位符,实际数据存储在 save_post 钩子中处理
    return null;
  },
} );

注意,save 函数返回 null。这是因为我们并不想直接在 post_content 中存储数据,而是想将数据存储到自定义字段中。

最后,你需要使用 save_post 钩子来更新自定义字段:

add_action( 'save_post', 'my_plugin_save_post' );

function my_plugin_save_post( $post_id, $post ) {
  // 检查是否是自动保存
  if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
    return;
  }

  // 检查用户是否有权限编辑文章
  if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
  }

  // 获取文章内容
  $content = $post->post_content;

  // 解析区块
  $blocks = parse_blocks( $content );

  // 遍历区块,找到我们的自定义区块
  foreach ( $blocks as $block ) {
    if ( $block['blockName'] === 'my-plugin/custom-block' ) {
      // 获取自定义属性的值
      $custom_text = isset( $block['attrs']['my_custom_text'] ) ? $block['attrs']['my_custom_text'] : '';

      // 更新自定义字段
      update_post_meta( $post_id, '_my_custom_block_text', $custom_text );
    }
  }
}

这段代码会解析文章内容,找到 my-plugin/custom-block 区块,获取 my_custom_text 属性的值,并将它存储到 _my_custom_block_text 自定义字段中。

第六幕:一些需要注意的点

  • 安全第一:save_post 钩子的回调函数中,一定要进行安全检查,防止恶意代码的执行。
  • 性能优化: save_post 钩子会在每次文章保存时被调用,所以你的代码一定要高效,避免影响网站的性能。
  • 版本兼容性: WordPress 的 API 会不断更新,所以你的代码要保持版本兼容性,避免在未来的版本中出现问题。
  • 调试技巧: 使用 error_log() 函数可以方便地调试 save_post 钩子的回调函数。

第七幕:总结与展望

save_post 钩子是 WordPress 中一个非常强大的工具,它可以让你在文章保存时进行各种各样的操作。在古腾堡编辑器中,save_post 钩子更是扮演着至关重要的角色,它负责处理区块数据的存储和更新。

通过深入理解 save_post 钩子的原理和用法,你可以更好地开发自定义区块,并实现各种各样的定制功能。

希望今天的讲座对大家有所帮助。如果有什么问题,欢迎在评论区留言。下次再见!

表格:save_post 钩子相关函数

函数名 作用 文件路径
add_action() 注册一个函数,让它在指定的动作(钩子)发生时被调用。 wp-includes/plugin.php
wp_insert_post() 核心函数,负责将文章数据插入或更新到数据库。 wp-includes/post.php
parse_blocks() 将包含序列化区块的字符串解析成一个区块数组。 wp-includes/blocks.php
serialize_block() 将一个区块数组重新序列化成 HTML 注释。 wp-includes/blocks.php
register_post_meta() 注册文章的自定义字段,允许你存储与文章相关的额外数据。 确保设置 show_in_resttrue,以便在古腾堡编辑器中使用 REST API 访问自定义字段。 wp-includes/meta.php
update_post_meta() 更新指定文章的自定义字段的值。 wp-includes/meta.php
get_post_meta() 获取指定文章的自定义字段的值。 wp-includes/meta.php
current_user_can() 检查当前用户是否具有执行指定操作的权限。这是一个重要的安全措施,可以防止未经授权的用户修改文章数据。 wp-includes/capabilities.php
defined('DOING_AUTOSAVE') && DOING_AUTOSAVE 用于检查当前是否是自动保存操作。 如果是自动保存,通常应该跳过一些比较耗时的操作,以避免影响编辑器的性能。 N/A

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注