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
中定义的 type
和 default
值,将属性值转换为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内容中提取属性值。它通常与 selector
和 attribute
属性一起使用。
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' );
说明:
attributes.posts
的source
设置为"query"
。query
对象定义了要查询的文章的参数,例如per_page
,order
,orderby
,post_type
。- 在
query
对象内部,你可以定义要从每个文章中提取的属性,例如title
,excerpt
,link
。 每个属性都指定了source
和selector
,类似于之前讨论的source: 'text'
和source: 'attribute'
。 edit
函数返回一个占位符,因为我们在编辑器中无法动态地显示文章列表。save
函数返回null
,因为文章列表将在前端动态地生成。render_callback
函数在PHP中执行,它使用get_posts
函数查询文章,并生成HTML代码。- 这个例子演示了如何使用
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
属性。这可以确保即使在数据库中没有存储属性值,区块也能正常显示。 - 避免存储不必要的数据: 只存储区块实际需要的属性值。避免存储冗余或可计算的数据。
- 考虑性能: 复杂的序列化和反序列化逻辑可能会影响编辑器的性能。尽量简化逻辑,并避免在
edit
和save
函数中执行耗时的操作。 - 数据验证和清理: 在反序列化属性值后,进行数据验证和清理,以确保数据的有效性和安全性。
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;
}
}
在这个例子中,我们定义了三个属性:imageId
,imageUrl
和 imageAlt
。imageId
存储图像的ID,imageUrl
存储图像的URL,imageAlt
存储图像的alt文本。在 edit
函数中,我们使用 MediaPlaceholder
组件来选择图像,并将图像的ID、URL和alt文本存储在属性中。在 save
函数中,我们使用 img
标签来显示图像。
8. 常见问题和调试技巧
- 属性值未正确保存: 检查
block.json
文件中的attributes
定义是否正确。确保type
属性与实际数据类型匹配。 - 编辑器中显示不正确: 检查
edit
函数中的代码是否正确。确保正确地处理属性值,并将其显示在编辑器中。 - 前端显示不正确: 检查
save
函数中的代码是否正确。确保正确地使用属性值,并生成正确的HTML代码。 - 使用
console.log
进行调试: 在edit
和save
函数中使用console.log
来输出属性值,以便检查它们是否正确。 - 检查HTML注释: 查看
post_content
字段中的HTML注释,以检查序列化后的属性值是否正确。
总结
今天我们深入探讨了Gutenberg区块中自定义属性的序列化和反序列化。我们学习了如何使用 attributes
对象定义区块属性,以及如何使用 source
属性自定义序列化和反序列化过程。掌握这些概念对于构建功能丰富、数据持久化的Gutenberg区块至关重要。通过仔细定义属性,并选择合适的序列化和反序列化策略,我们可以确保区块的数据能够正确地存储和检索。