Gutenberg区块:如何处理自定义属性的序列化和反序列化,并确保内容兼容性?

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) 和其他可选参数,例如 sourceselectorsourceselector 决定了属性值从哪里提取。

  • 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 被设置为 htmlselector 被设置为 p。这意味着 Gutenberg 会在保存的 HTML 中寻找 <p> 标签,并将其内容提取出来作为 content 属性的值。

  • alignment 属性没有 sourceselector。这意味着 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 会:

  1. 找到 <p class="wp-block-my-custom-block-basic-text"> 标签。
  2. 提取 <p> 标签的内容,并赋值给 content 属性。
  3. 提取 <p> 标签的 style 属性中的 text-align 值,并赋值给 alignment 属性。

3. 自定义属性的 Source 和 Selector

sourceselectorattributes 定义中两个关键的参数,它们控制了属性值如何从保存的 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 被设置为 attributeselector 被设置为 imgattribute 被设置为 src。这意味着 Gutenberg 会在保存的 HTML 中寻找 <img> 标签,并将其 src 属性的值提取出来作为 imageUrl 属性的值。
  • altText 属性的 source 被设置为 attributeselector 被设置为 imgattribute 被设置为 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 会:

  1. 找到 <img class="wp-block-my-custom-block-image-block"> 标签。
  2. 提取 <img> 标签的 src 属性的值,并赋值给 imageUrl 属性。
  3. 提取 <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 对象。

我们可以使用 serializeparse 函数来实现自定义的序列化和反序列化逻辑。

// 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. 表格总结:常用技巧与最佳实践

技巧/实践 描述 适用场景
使用合适的 sourceselector 根据属性值的来源选择合适的 source 类型,并使用 selector 精确地定位到要提取值的 HTML 元素。 从 HTML 元素中提取属性值,例如图片 URL、文本内容等。
使用 migrate 属性 在属性定义中,使用 migrate 属性来指定一个函数,该函数负责将旧版本的属性值转换为新版本的属性值。 在区块版本更新时,需要迁移旧版本的属性值。
检查区块版本 edit 函数中,检查区块的版本,并根据版本执行不同的逻辑。 在区块版本更新时,需要根据版本显示不同的界面或执行不同的操作。
使用 innerBlocksInnerBlocks 处理嵌套区块的序列化和反序列化。 区块包含其他区块,例如容器区块、列区块等。
使用 HTML 注释存储复杂数据 使用 HTML 注释来存储一些无法直接嵌入到 HTML 元素中的数据,例如复杂的 JavaScript 对象。 需要存储复杂的数据结构,例如 JSON 对象。
保持 save 函数的简洁性 save 函数应该只负责渲染 HTML,不应该包含任何复杂的逻辑。属性的序列化应该在 attributes 定义中使用 serialize 函数来完成。 提高代码的可读性和可维护性。
充分测试与验证 在发布区块之前,务必进行充分的测试和验证,确保区块在各种情况下都能正常工作。 避免在生产环境中出现问题。

属性处理是区块开发的关键

总而言之,区块属性的序列化和反序列化是 Gutenberg 区块开发的核心环节。理解这些概念,掌握各种 source 类型和技巧,并遵循最佳实践,可以帮助我们构建更加健壮、灵活和可维护的区块。同时,注意内容兼容性,让你的区块能够平滑过渡到新的版本,保证用户的内容不会丢失或损坏。

发表回复

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