Gutenberg区块:如何处理自定义属性的序列化和反序列化?

Gutenberg 区块:自定义属性的序列化与反序列化

大家好!今天我们要深入探讨Gutenberg区块开发中一个至关重要的方面:自定义属性的序列化与反序列化。理解并掌握这个概念,对于构建功能丰富、数据持久化的Gutenberg区块至关重要。

1. 什么是序列化与反序列化?

在深入Gutenberg区块的细节之前,我们先回顾一下序列化和反序列化的基本概念。

  • 序列化 (Serialization): 将对象(在这里,指的是区块属性)转换为一种可以存储或传输的格式。通常,这种格式是字符串,例如JSON。

  • 反序列化 (Deserialization): 将序列化后的数据(例如JSON字符串)转换回对象(区块属性)。

在Gutenberg区块的上下文中,序列化是将区块的属性值转换为可存储在WordPress数据库中的格式。反序列化则是从数据库中检索数据,并将其转换回区块编辑器可以使用的属性值。

2. Gutenberg区块属性的存储机制

Gutenberg区块的属性存储在WordPress的 post_content 字段中,使用HTML注释的形式。这种方式确保了即使在没有Gutenberg编辑器的情况下,内容仍然可以被解析和显示,尽管可能无法完全还原编辑时的状态。

例如,一个包含标题和正文的简单区块,其序列化后的数据可能如下所示:

<!-- wp:my-custom-block {"title":"我的标题","content":"这是正文内容"} /-->

wp:my-custom-block 是区块的名称,{"title":"我的标题","content":"这是正文内容"} 则是序列化后的属性,采用了JSON格式。

3. attributes 对象:定义区块属性

block.json 文件中,或者通过 registerBlockType 函数,我们使用 attributes 对象来定义区块的属性。这个对象描述了每个属性的类型、默认值,以及在序列化和反序列化过程中如何处理它们。

{
  "name": "my-custom-block",
  "title": "我的自定义区块",
  "attributes": {
    "title": {
      "type": "string",
      "default": "默认标题"
    },
    "content": {
      "type": "string",
      "default": ""
    },
    "image": {
      "type": "string",
      "default": null
    },
    "booleanAttribute": {
      "type": "boolean",
      "default": false
    },
    "numericAttribute": {
      "type": "number",
      "default": 0
    },
    "arrayAttribute": {
      "type": "array",
      "default": []
    },
    "objectAttribute": {
      "type": "object",
      "default": {}
    }
  },
  "supports": {
    "align": true
  },
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

在这个例子中,我们定义了多个属性,包括字符串、布尔值、数字、数组和对象。type 属性指定了属性的数据类型,default 属性指定了属性的默认值。这些信息对于序列化和反序列化过程至关重要。

4. 默认的序列化和反序列化过程

Gutenberg编辑器会自动处理大部分属性的序列化和反序列化。对于简单的属性类型,如字符串、数字、布尔值、数组和对象,通常不需要额外的代码。编辑器会按照 block.json 中定义的 typedefault 值,将属性值转换为JSON字符串,并存储在HTML注释中。

例如,如果 title 属性的值为 "新的标题",content 属性的值为 "新的内容",那么序列化后的数据可能如下所示:

<!-- wp:my-custom-block {"title":"新的标题","content":"新的内容"} /-->

当编辑器加载页面时,它会解析HTML注释,提取JSON数据,并将其反序列化为区块的属性值。

5. 自定义序列化和反序列化:source 属性

在某些情况下,我们需要更精细地控制属性的序列化和反序列化过程。例如:

  • 从HTML元素中提取属性值。
  • 组合多个属性值来生成一个属性值。
  • 对属性值进行格式化或转换。

这时,我们可以使用 attributes 对象中的 source 属性。source 属性指定了属性值的来源,以及如何从该来源提取属性值。

5.1 source: 'html'

source: 'html' 用于从区块的HTML内容中提取属性值。它通常与 selectorattribute 属性一起使用。

  • selector: 指定要从中提取属性值的HTML元素的CSS选择器。
  • attribute: 指定要提取的HTML元素的属性名称。如果省略 attribute,则提取HTML元素的 innerHTML

例如,假设我们有一个自定义区块,它包含一个带有 data-title 属性的 h2 元素:

<h2 data-title="自定义标题"></h2>

我们可以使用以下 attributes 定义来提取 data-title 属性的值:

{
  "name": "my-custom-block",
  "title": "我的自定义区块",
  "attributes": {
    "title": {
      "type": "string",
      "source": "attribute",
      "selector": "h2",
      "attribute": "data-title"
    }
  },
  "edit": (props) => {
    const { attributes, setAttributes } = props;
    const { title } = attributes;

    const onChangeTitle = (newTitle) => {
      setAttributes({ title: newTitle });
    };

    return (
      <div>
        <h2 data-title={title} onChange={(e) => onChangeTitle(e.target.value)}>{title}</h2>
      </div>
    );
  },
  "save": (props) => {
    const { attributes } = props;
    const { title } = attributes;

    return (
      <h2 data-title={title}>{title}</h2>
    );
  }
}

在这个例子中,source: 'attribute' 告诉编辑器从HTML元素的属性中提取 title 的值。selector: 'h2' 指定要从 h2 元素中提取属性值,attribute: 'data-title' 指定要提取 data-title 属性的值。

5.2 source: 'text'

source: 'text' 用于从区块的HTML内容的文本节点中提取属性值。它通常与 selector 属性一起使用。

  • selector: 指定要从中提取文本的HTML元素的CSS选择器。

例如,假设我们有一个自定义区块,它包含一个 p 元素:

<p>这是段落内容</p>

我们可以使用以下 attributes 定义来提取 p 元素的文本内容:

{
  "name": "my-custom-block",
  "title": "我的自定义区块",
  "attributes": {
    "content": {
      "type": "string",
      "source": "text",
      "selector": "p"
    }
  },
  "edit": (props) => {
    const { attributes, setAttributes } = props;
    const { content } = attributes;

    const onChangeContent = (newContent) => {
      setAttributes({ content: newContent });
    };

    return (
      <div>
        <p onChange={(e) => onChangeContent(e.target.value)}>{content}</p>
      </div>
    );
  },
  "save": (props) => {
    const { attributes } = props;
    const { content } = attributes;

    return (
      <p>{content}</p>
    );
  }
}

在这个例子中,source: 'text' 告诉编辑器从HTML元素的文本节点中提取 content 的值。selector: 'p' 指定要从 p 元素中提取文本。

5.3 source: 'meta'

source: 'meta' 用于从文章的自定义字段(meta fields)中提取属性值。它需要与 meta 属性一起使用。

  • meta: 指定要从中提取属性值的自定义字段的名称。

首先,你需要注册自定义字段。这可以通过 register_post_meta 函数在PHP中完成。

<?php
function my_custom_block_register_meta() {
  register_post_meta( 'post', 'my_custom_title', array(
    'show_in_rest' => true,
    'type' => 'string',
    'single' => true,
    'auth_callback' => function() {
      return current_user_can( 'edit_posts' );
    }
  ));
}
add_action( 'init', 'my_custom_block_register_meta' );

然后,你可以在 block.json 文件中使用 source: 'meta' 来提取自定义字段的值:

{
  "name": "my-custom-block",
  "title": "我的自定义区块",
  "attributes": {
    "customTitle": {
      "type": "string",
      "source": "meta",
      "meta": "my_custom_title"
    }
  },
  "edit": (props) => {
    const { attributes, setAttributes, meta } = props;
    const { customTitle } = attributes;

    const onChangeCustomTitle = (newCustomTitle) => {
      setAttributes({ customTitle: newCustomTitle });
      props.setMetaValue('my_custom_title', newCustomTitle);
    };

    return (
      <div>
        <label htmlFor="custom-title">自定义标题:</label>
        <input
          type="text"
          id="custom-title"
          value={customTitle || meta['my_custom_title'] || ''}
          onChange={(e) => onChangeCustomTitle(e.target.value)}
        />
      </div>
    );
  },
  "save": (props) => {
    return null; // 不在内容中渲染,只存储在meta中
  }
}

在这个例子中,source: 'meta' 告诉编辑器从文章的自定义字段中提取 customTitle 的值。meta: 'my_custom_title' 指定要从名为 my_custom_title 的自定义字段中提取值。

注意: save 函数返回 null,因为我们不想在 post_content 中存储这个属性,而是仅仅依赖于 post_meta 来存储。

5.4 source: 'query'

source: 'query' 用于从文章的查询结果中提取属性值。它需要与 query 属性一起使用。这种用法比较复杂,通常用于动态地从WordPress的其他文章或页面中获取数据。

{
  "name": "my-custom-block",
  "title": "我的文章列表区块",
  "attributes": {
    "posts": {
      "type": "array",
      "source": "query",
      "selector": "article",
      "query": {
        "per_page": 3,
        "order": "desc",
        "orderby": "date",
        "post_type": "post",
        "title": {
          "source": "text",
          "selector": "h2 a"
        },
        "excerpt": {
          "source": "text",
          "selector": "p"
        },
        "link": {
          "source": "attribute",
          "selector": "h2 a",
          "attribute": "href"
        }
      }
    }
  },
  "edit": (props) => {
    // 编辑器中的占位符
    return <p>文章列表 (仅在前端显示)</p>;
  },
  "save": (props) => {
    return null; // 前端动态生成
  }
}

PHP 端动态渲染(render_callback):

<?php
function render_my_posts_block( $attributes, $content, $block ) {
  $args = array(
    'posts_per_page' => 3,
    'orderby'        => 'date',
    'order'          => 'DESC',
  );

  $posts = get_posts( $args );

  if ( empty( $posts ) ) {
    return '<p>没有找到文章。</p>';
  }

  $output = '<div class="my-posts-block">';
  foreach ( $posts as $post ) {
    $output .= '<article>';
    $output .= '<h2><a href="' . get_permalink( $post->ID ) . '">' . get_the_title( $post->ID ) . '</a></h2>';
    $output .= '<p>' . get_the_excerpt( $post->ID ) . '</p>';
    $output .= '</article>';
  }
  $output .= '</div>';

  return $output;
}

function register_my_custom_blocks() {
  register_block_type( 'my-custom-block/my-posts-block', array(
    'render_callback' => 'render_my_posts_block',
  ) );
}
add_action( 'init', 'register_my_custom_blocks' );

说明:

  1. attributes.postssource 设置为 "query"
  2. query 对象定义了要查询的文章的参数,例如 per_pageorderorderbypost_type
  3. query 对象内部,你可以定义要从每个文章中提取的属性,例如 titleexcerptlink。 每个属性都指定了 sourceselector,类似于之前讨论的 source: 'text'source: 'attribute'
  4. edit 函数返回一个占位符,因为我们在编辑器中无法动态地显示文章列表。
  5. save 函数返回 null,因为文章列表将在前端动态地生成。
  6. render_callback 函数在PHP中执行,它使用 get_posts 函数查询文章,并生成HTML代码。
  7. 这个例子演示了如何使用 source: 'query' 来从文章的查询结果中提取属性值,并动态地生成区块的HTML代码。

5.5 source: 'attribute' (修正)

这个属性前面已经描述过了,这里补充一个细节。需要注意的是,当使用source: 'attribute'时,如果省略了attribute属性,则默认提取元素的innerHTML。这与source: 'text'不同,source: 'text'总是提取文本内容,而忽略HTML标签。

例如:

<div class="my-block">
  <h1>这是一个标题</h1>
  <p>这是一段文字。</p>
</div>

如果attributes定义如下:

{
  "content": {
    "type": "string",
    "source": "attribute",
    "selector": ".my-block"
  }
}

那么,content属性的值将会是:<h1>这是一个标题</h1>n<p>这是一段文字。</p>

而如果attributes定义如下:

{
  "content": {
    "type": "string",
    "source": "text",
    "selector": ".my-block"
  }
}

那么,content属性的值将会是:这是一个标题n这是一段文字。

6. 序列化和反序列化的最佳实践

  • 明确定义属性类型:block.json 文件中,始终为每个属性指定 type 属性。这有助于编辑器正确地序列化和反序列化属性值。
  • 使用默认值: 为每个属性指定 default 属性。这可以确保即使在数据库中没有存储属性值,区块也能正常显示。
  • 避免存储不必要的数据: 只存储区块实际需要的属性值。避免存储冗余或可计算的数据。
  • 考虑性能: 复杂的序列化和反序列化逻辑可能会影响编辑器的性能。尽量简化逻辑,并避免在 editsave 函数中执行耗时的操作。
  • 数据验证和清理: 在反序列化属性值后,进行数据验证和清理,以确保数据的有效性和安全性。

7. 一个更复杂的例子:图像区块

让我们看一个更复杂的例子,一个可以插入图像的区块。这个区块需要存储图像的ID、URL和alt文本。

{
  "name": "my-image-block",
  "title": "我的图像区块",
  "attributes": {
    "imageId": {
      "type": "number",
      "default": null
    },
    "imageUrl": {
      "type": "string",
      "default": ""
    },
    "imageAlt": {
      "type": "string",
      "default": ""
    }
  },
  "edit": (props) => {
    const { attributes, setAttributes } = props;
    const { imageId, imageUrl, imageAlt } = attributes;

    const onSelectImage = (image) => {
      setAttributes({
        imageId: image.id,
        imageUrl: image.url,
        imageAlt: image.alt
      });
    };

    return (
      <div>
        {imageUrl ? (
          <img src={imageUrl} alt={imageAlt} />
        ) : (
          <MediaPlaceholder
            onSelectMedia={onSelectImage}
            allowedTypes={['image']}
            multiple={false}
            labels={{ title: '选择图像' }}
          />
        )}
      </div>
    );
  },
  "save": (props) => {
    const { attributes } = props;
    const { imageUrl, imageAlt } = attributes;

    return imageUrl ? (
      <img src={imageUrl} alt={imageAlt} />
    ) : null;
  }
}

在这个例子中,我们定义了三个属性:imageIdimageUrlimageAltimageId 存储图像的ID,imageUrl 存储图像的URL,imageAlt 存储图像的alt文本。在 edit 函数中,我们使用 MediaPlaceholder 组件来选择图像,并将图像的ID、URL和alt文本存储在属性中。在 save 函数中,我们使用 img 标签来显示图像。

8. 常见问题和调试技巧

  • 属性值未正确保存: 检查 block.json 文件中的 attributes 定义是否正确。确保 type 属性与实际数据类型匹配。
  • 编辑器中显示不正确: 检查 edit 函数中的代码是否正确。确保正确地处理属性值,并将其显示在编辑器中。
  • 前端显示不正确: 检查 save 函数中的代码是否正确。确保正确地使用属性值,并生成正确的HTML代码。
  • 使用 console.log 进行调试:editsave 函数中使用 console.log 来输出属性值,以便检查它们是否正确。
  • 检查HTML注释: 查看 post_content 字段中的HTML注释,以检查序列化后的属性值是否正确。

总结

今天我们深入探讨了Gutenberg区块中自定义属性的序列化和反序列化。我们学习了如何使用 attributes 对象定义区块属性,以及如何使用 source 属性自定义序列化和反序列化过程。掌握这些概念对于构建功能丰富、数据持久化的Gutenberg区块至关重要。通过仔细定义属性,并选择合适的序列化和反序列化策略,我们可以确保区块的数据能够正确地存储和检索。

发表回复

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