Gutenberg 区块:自定义属性序列化与反序列化,兼顾内容兼容性
各位同学,大家好!今天我们来深入探讨 Gutenberg 区块开发中一个至关重要的方面:自定义属性的序列化和反序列化,以及如何确保内容在不同版本的区块之间保持兼容性。
Gutenberg 编辑器,作为 WordPress 的现代内容编辑体验,完全基于区块的概念。每个区块负责渲染页面的一小部分内容。而区块的行为和外观,很大程度上取决于它的属性。这些属性定义了区块包含的数据,例如文本内容、图片 URL、颜色设置等。当我们将区块插入到文章中,这些属性需要被保存到数据库,并在后续编辑或渲染时正确地加载出来。这个过程就涉及到序列化和反序列化。
1. 序列化与反序列化的基本概念
简单来说:
-
序列化 (Serialization):将区块的属性数据转换为可以存储或传输的格式。在 Gutenberg 中,通常是将 JavaScript 对象形式的属性数据转换为 HTML 注释或 JSON 字符串,然后嵌入到文章内容中。
-
反序列化 (Deserialization):将存储或传输的格式的数据转换回区块的属性数据。在 Gutenberg 中,就是从文章内容中提取 HTML 注释或 JSON 字符串,解析成 JavaScript 对象,并赋值给区块的属性。
理解这两个概念是掌握区块属性处理的基础。
2. Gutenberg 如何处理属性的序列化和反序列化
Gutenberg 默认提供了一套机制来处理属性的序列化和反序列化。它主要依赖于区块的 attributes
定义和 save
函数。
-
attributes
定义:在block.json
文件(或者旧版本使用registerBlockType
函数)中,我们定义了区块的属性。每个属性都有一个类型 (type) 和其他可选参数,例如source
和selector
。source
和selector
决定了属性值从哪里提取。 -
save
函数:save
函数定义了区块在保存时如何渲染成 HTML。这个函数返回的 HTML 会被存储到文章内容中。同时,save
函数也负责将属性值以某种形式嵌入到 HTML 中,以便后续的反序列化。
下面是一个简单的例子:
// block.json (或 registerBlockType 的配置对象)
{
"name": "my-custom-block/basic-text",
"title": "Basic Text Block",
"category": "common",
"icon": "text",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
},
"alignment": {
"type": "string",
"default": "left"
}
},
"supports": {
"align": true
},
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
// 编辑器组件 (edit.js)
import { useBlockProps, RichText, AlignmentControl } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
export default function Edit( { attributes, setAttributes } ) {
const { content, alignment } = attributes;
const blockProps = useBlockProps();
const onChangeContent = ( newContent ) => {
setAttributes( { content: newContent } );
};
const onChangeAlignment = ( newAlignment ) => {
setAttributes( { alignment: newAlignment } );
};
return (
<div { ...blockProps }>
<AlignmentControl value={ alignment } onChange={ onChangeAlignment } />
<RichText
tagName="p"
value={ content }
onChange={ onChangeContent }
style={ { textAlign: alignment } }
placeholder="Enter your text here..."
/>
</div>
);
}
// 保存组件 (save.js)
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { content, alignment } = attributes;
return (
<p { ...useBlockProps.save( { style: { textAlign: alignment } } ) }>
{ content }
</p>
);
}
在这个例子中:
-
content
属性的source
被设置为html
,selector
被设置为p
。这意味着 Gutenberg 会在保存的 HTML 中寻找<p>
标签,并将其内容提取出来作为content
属性的值。 -
alignment
属性没有source
和selector
。这意味着 Gutenberg 会直接使用attributes.alignment
的值。在save
函数中,我们将alignment
属性的值作为style
属性应用到<p>
标签上。
当文章保存时,生成的 HTML 可能是这样的:
<p class="wp-block-my-custom-block-basic-text" style="text-align: left;">Hello, world!</p>
当文章被加载时,Gutenberg 会:
- 找到
<p class="wp-block-my-custom-block-basic-text">
标签。 - 提取
<p>
标签的内容,并赋值给content
属性。 - 提取
<p>
标签的style
属性中的text-align
值,并赋值给alignment
属性。
3. 自定义属性的 Source 和 Selector
source
和 selector
是 attributes
定义中两个关键的参数,它们控制了属性值如何从保存的 HTML 中提取。Gutenberg 提供了多种 source
类型,每种类型都有不同的提取方式。
下面是一些常用的 source
类型:
Source 类型 | 描述 | 例子 |
---|---|---|
html |
从指定 selector 匹配的 HTML 元素的 innerHTML 中提取值。 | "source": "html", "selector": "p" :提取 <p> 标签的内容。 |
text |
从指定 selector 匹配的 HTML 元素的 textContent 中提取值。与 html 类似,但会去除 HTML 标签。 |
"source": "text", "selector": "h2" :提取 <h2> 标签的文本内容。 |
attribute |
从指定 selector 匹配的 HTML 元素的指定属性中提取值。 | "source": "attribute", "selector": "img", "attribute": "src" :提取 <img> 标签的 src 属性值。 |
meta |
从文章的自定义字段 (meta field) 中提取值。需要配置 meta 属性,并确保用户有权限编辑该自定义字段。 |
"source": "meta", "meta": "my_custom_meta_key" :从 my_custom_meta_key 自定义字段中提取值。 |
query |
从指定 selector 匹配的 HTML 元素中提取多个值,并将它们组成一个数组。通常用于处理列表或重复的内容。 | "source": "query", "selector": "li", "query": { "text": { "source": "text" } } :提取 <li> 标签的文本内容,并将它们组成一个数组。 |
block |
将整个嵌套的区块作为属性值。通常用于处理内部区块。 | "source": "block", "selector": ".wp-block-my-inner-block" :提取 .wp-block-my-inner-block 元素包含的整个区块。 |
selector
参数是一个 CSS 选择器,用于指定要提取值的 HTML 元素。如果 selector
没有指定,Gutenberg 会默认使用区块的根元素。
代码示例:使用 attribute
source 提取图片 URL
假设我们有一个图片区块,需要提取图片的 URL。
// block.json
{
"name": "my-custom-block/image-block",
"title": "Image Block",
"category": "common",
"icon": "format-image",
"attributes": {
"imageUrl": {
"type": "string",
"source": "attribute",
"selector": "img",
"attribute": "src"
},
"altText": {
"type": "string",
"source": "attribute",
"selector": "img",
"attribute": "alt"
}
},
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
// 编辑器组件 (edit.js)
import { useBlockProps, MediaPlaceholder, BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const { imageUrl, altText } = attributes;
const blockProps = useBlockProps();
const onSelectImage = ( media ) => {
setAttributes( {
imageUrl: media.url,
altText: media.alt
} );
};
const onRemoveImage = () => {
setAttributes( {
imageUrl: null,
altText: null
} );
};
return (
<div { ...blockProps }>
<BlockControls>
<ToolbarGroup>
{ imageUrl && (
<ToolbarButton onClick={ onRemoveImage } icon="trash" label="Remove Image" />
) }
</ToolbarGroup>
</BlockControls>
{ ! imageUrl ? (
<MediaPlaceholder
onSelect={ onSelectImage }
accept="image/*"
allowedTypes={ [ 'image' ] }
multiple={ false }
labels={ {
title: 'Image',
instructions: 'Drag an image here, or select one from your media library.'
} }
/>
) : (
<img src={ imageUrl } alt={ altText } />
) }
</div>
);
}
// 保存组件 (save.js)
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { imageUrl, altText } = attributes;
return (
<img { ...useBlockProps.save( { src: imageUrl, alt: altText } ) } />
);
}
在这个例子中:
imageUrl
属性的source
被设置为attribute
,selector
被设置为img
,attribute
被设置为src
。这意味着 Gutenberg 会在保存的 HTML 中寻找<img>
标签,并将其src
属性的值提取出来作为imageUrl
属性的值。altText
属性的source
被设置为attribute
,selector
被设置为img
,attribute
被设置为alt
。 这意味着Gutenberg 会在保存的HTML中寻找<img>
标签,并将其alt
属性的值提取出来作为altText
属性的值。
当文章保存时,生成的 HTML 可能是这样的:
<img class="wp-block-my-custom-block-image-block" src="https://example.com/my-image.jpg" alt="My Image" />
当文章被加载时,Gutenberg 会:
- 找到
<img class="wp-block-my-custom-block-image-block">
标签。 - 提取
<img>
标签的src
属性的值,并赋值给imageUrl
属性。 - 提取
<img>
标签的alt
属性的值,并赋值给altText
属性。
4. 内容兼容性:处理区块版本的更新
随着区块功能的不断完善,我们可能需要修改区块的属性定义或 save
函数。这可能会导致旧版本的区块在新的 Gutenberg 编辑器中无法正确显示或编辑。为了解决这个问题,我们需要采取一些措施来确保内容兼容性。
以下是一些常用的技巧:
-
使用
migrate
属性:在attributes
定义中,我们可以使用migrate
属性来指定一个函数,该函数负责将旧版本的属性值转换为新版本的属性值。 -
检查区块版本:在
edit
函数中,我们可以检查区块的版本,并根据版本执行不同的逻辑。 -
提供迁移工具:如果迁移逻辑比较复杂,我们可以提供一个单独的迁移工具,让用户手动将旧版本的区块转换为新版本的区块。
代码示例:使用 migrate
属性迁移属性值
假设我们最初的区块只有一个 content
属性,后来我们添加了一个 alignment
属性。我们需要将旧版本的区块的 content
属性居中对齐。
// block.json
{
"name": "my-custom-block/text-block",
"title": "Text Block",
"category": "common",
"icon": "text",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
},
"alignment": {
"type": "string",
"default": "left",
"migrate": ( attributes ) => {
if ( ! attributes.alignment ) {
return { alignment: 'center' };
}
return attributes;
}
}
},
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"version": 2
}
在这个例子中:
- 我们为
alignment
属性添加了一个migrate
函数。这个函数接收旧版本的属性作为参数,如果alignment
属性不存在,则将其设置为center
。 - 我们还添加了
version
属性,并且数值为2。
当旧版本的区块(没有 alignment
属性)被加载时,migrate
函数会被调用,并将 alignment
属性设置为 center
。
代码示例:检查区块版本并执行不同的逻辑
假设我们在区块的某个版本中引入了一个新的属性,我们需要在 edit
函数中检查区块的版本,并根据版本显示不同的界面。
// 编辑器组件 (edit.js)
import { useBlockProps, RichText, AlignmentControl } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
export default function Edit( { attributes, setAttributes, block } ) {
const { content, alignment, newProperty } = attributes;
const { version } = block;
const blockProps = useBlockProps();
const onChangeContent = ( newContent ) => {
setAttributes( { content: newContent } );
};
const onChangeAlignment = ( newAlignment ) => {
setAttributes( { alignment: newAlignment } );
};
const onChangeNewProperty = ( newNewProperty ) => {
setAttributes( { newProperty: newNewProperty } );
};
return (
<div { ...blockProps }>
<AlignmentControl value={ alignment } onChange={ onChangeAlignment } />
<RichText
tagName="p"
value={ content }
onChange={ onChangeContent }
style={ { textAlign: alignment } }
placeholder="Enter your text here..."
/>
{ version >= 2 && (
<div>
<label htmlFor="new-property">New Property:</label>
<input
type="text"
id="new-property"
value={ newProperty }
onChange={ ( event ) => onChangeNewProperty( event.target.value ) }
/>
</div>
) }
</div>
);
}
在这个例子中:
- 我们在
edit
函数中通过block.version
获取区块的版本。 - 如果区块的版本大于等于 2,则显示一个新的输入框,用于编辑
newProperty
属性。
5. 更复杂的序列化场景:处理嵌套区块
Gutenberg 支持嵌套区块,这意味着一个区块可以包含其他区块。处理嵌套区块的序列化和反序列化需要使用 innerBlocks
属性和 InnerBlocks
组件。
-
innerBlocks
属性:在attributes
定义中,我们可以使用innerBlocks
属性来指定一个属性,该属性用于存储内部区块。 -
InnerBlocks
组件:在edit
函数中,我们可以使用InnerBlocks
组件来渲染内部区块。在save
函数中,我们需要使用InnerBlocks.Content
组件来保存内部区块的内容。
代码示例:处理嵌套区块
假设我们有一个容器区块,可以包含其他区块。
// block.json (容器区块)
{
"name": "my-custom-block/container-block",
"title": "Container Block",
"category": "common",
"icon": "layout",
"attributes": {
"innerBlocks": {
"type": "array",
"default": []
}
},
"supports": {
"inserter": true,
"align": true,
"mode": false,
"multiple": true,
"reusable": true,
"html": false
},
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
// 编辑器组件 (edit.js)
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<InnerBlocks />
</div>
);
}
// 保存组件 (save.js)
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function save( { attributes } ) {
return (
<div { ...useBlockProps.save() }>
<InnerBlocks.Content />
</div>
);
}
在这个例子中:
- 我们在容器区块的
attributes
定义中添加了一个innerBlocks
属性,用于存储内部区块。 - 在
edit
函数中,我们使用InnerBlocks
组件来渲染内部区块。 - 在
save
函数中,我们使用InnerBlocks.Content
组件来保存内部区块的内容.
6. 使用HTML注释来存储属性值
在一些情况下,我们可能需要使用 HTML 注释来存储属性值。这通常用于存储一些无法直接嵌入到 HTML 元素中的数据,例如复杂的 JavaScript 对象。
我们可以使用 serialize
和 parse
函数来实现自定义的序列化和反序列化逻辑。
// block.json
{
"name": "my-custom-block/complex-data-block",
"title": "Complex Data Block",
"category": "common",
"icon": "data",
"attributes": {
"complexData": {
"type": "object",
"default": {},
"source": "html",
"selector": ".complex-data-container",
"serialize": ( attributes ) => {
const { complexData } = attributes;
return `<div class="complex-data-container"><!-- wp:my-custom-block/complex-data-block:data ${ JSON.stringify( complexData ) } /--></div>`;
},
"parse": ( element ) => {
try {
const comment = element.firstChild;
const jsonString = comment.textContent.match( /^{.*}$/ ) ? comment.textContent : null;
return jsonString ? JSON.parse( jsonString ) : {};
} catch ( error ) {
return {};
}
}
}
},
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
// 编辑器组件 (edit.js)
import { useBlockProps } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
export default function Edit( { attributes, setAttributes } ) {
const { complexData } = attributes;
const [ data, setData ] = useState( complexData );
const blockProps = useBlockProps();
const onChangeData = ( key, value ) => {
const newData = { ...data, [ key ]: value };
setData( newData );
setAttributes( { complexData: newData } );
};
return (
<div { ...blockProps }>
<label htmlFor="data-field-1">Field 1:</label>
<input
type="text"
id="data-field-1"
value={ data.field1 || '' }
onChange={ ( event ) => onChangeData( 'field1', event.target.value ) }
/>
<label htmlFor="data-field-2">Field 2:</label>
<input
type="text"
id="data-field-2"
value={ data.field2 || '' }
onChange={ ( event ) => onChangeData( 'field2', event.target.value ) }
/>
</div>
);
}
// 保存组件 (save.js)
export default function save() {
return null; // 属性序列化在 attribute 中的 serialize 函数中完成,这里无需返回任何 HTML
}
在这个例子中:
- 我们定义了一个
complexData
属性,它的类型是object
。 - 我们使用了
serialize
函数来将complexData
属性转换为一个包含 JSON 字符串的 HTML 注释,并将该注释嵌入到一个<div>
标签中。 - 我们使用了
parse
函数来从 HTML 注释中提取 JSON 字符串,并将其解析为 JavaScript 对象。
使用这种方法,我们可以存储任何复杂的数据结构,而不用担心数据格式的问题。
7. 表格总结:常用技巧与最佳实践
技巧/实践 | 描述 | 适用场景 |
---|---|---|
使用合适的 source 和 selector |
根据属性值的来源选择合适的 source 类型,并使用 selector 精确地定位到要提取值的 HTML 元素。 |
从 HTML 元素中提取属性值,例如图片 URL、文本内容等。 |
使用 migrate 属性 |
在属性定义中,使用 migrate 属性来指定一个函数,该函数负责将旧版本的属性值转换为新版本的属性值。 |
在区块版本更新时,需要迁移旧版本的属性值。 |
检查区块版本 | 在 edit 函数中,检查区块的版本,并根据版本执行不同的逻辑。 |
在区块版本更新时,需要根据版本显示不同的界面或执行不同的操作。 |
使用 innerBlocks 和 InnerBlocks |
处理嵌套区块的序列化和反序列化。 | 区块包含其他区块,例如容器区块、列区块等。 |
使用 HTML 注释存储复杂数据 | 使用 HTML 注释来存储一些无法直接嵌入到 HTML 元素中的数据,例如复杂的 JavaScript 对象。 | 需要存储复杂的数据结构,例如 JSON 对象。 |
保持 save 函数的简洁性 |
save 函数应该只负责渲染 HTML,不应该包含任何复杂的逻辑。属性的序列化应该在 attributes 定义中使用 serialize 函数来完成。 |
提高代码的可读性和可维护性。 |
充分测试与验证 | 在发布区块之前,务必进行充分的测试和验证,确保区块在各种情况下都能正常工作。 | 避免在生产环境中出现问题。 |
属性处理是区块开发的关键
总而言之,区块属性的序列化和反序列化是 Gutenberg 区块开发的核心环节。理解这些概念,掌握各种 source
类型和技巧,并遵循最佳实践,可以帮助我们构建更加健壮、灵活和可维护的区块。同时,注意内容兼容性,让你的区块能够平滑过渡到新的版本,保证用户的内容不会丢失或损坏。