WordPress源码深度解析之:古腾堡的`InnerBlocks`:如何实现嵌套块的架构。

各位掘金的弄潮儿们,大家好!我是你们的老朋友,今天咱们来聊聊WordPress古腾堡编辑器里的一个重量级选手:InnerBlocks。这玩意儿,说白了,就是让你的块儿可以嵌套其他块儿,就像俄罗斯套娃一样,一层套一层,玩出各种花样。

开场白:为啥我们需要嵌套块?

想象一下,你正在构建一个精美的页面,你想要一个卡片布局,每张卡片里都要有标题、内容和按钮。如果没有InnerBlocks,你可能需要单独创建每个卡片,然后手动调整它们的样式和布局。这不仅繁琐,而且难以维护。

但有了InnerBlocks,你可以创建一个“卡片容器”块,然后允许用户在这个容器里添加标题、内容和按钮块。这样,整个卡片就是一个独立的单元,易于管理和复用。这就是嵌套块的魅力!

第一幕:InnerBlocks的基本概念

InnerBlocks是古腾堡编辑器提供的一个组件,它允许你定义一个块,该块可以包含其他块。它主要涉及两个方面:

  • 父块(Parent Block): 包含其他块的块,也就是“容器”。
  • 子块(Child Block): 被父块包含的块,也就是“内容”。

InnerBlocks组件的核心在于两个:

  • InnerBlocks.Content 用于在父块的编辑界面渲染子块。
  • InnerBlocks.BlockList 用于在父块的保存界面渲染子块。

第二幕:代码实战:创建一个简单的嵌套块

咱们先从一个最简单的例子开始,创建一个“容器”块,允许用户在其中添加段落块。

  1. 注册父块(容器块):
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

registerBlockType('my-plugin/container-block', {
  title: '容器块',
  icon: 'align-wide',
  category: 'common',
  edit: ({ className }) => {
    return (
      <div { ...useBlockProps({
        className: className,
      }) }>
        <h2>容器块</h2>
        <InnerBlocks />
      </div>
    );
  },
  save: ({ className }) => {
    return (
      <div { ...useBlockProps.save({
        className: className,
      }) }>
        <h2>容器块</h2>
        <InnerBlocks.Content />
      </div>
    );
  },
});

这段代码做了什么?

  • registerBlockType:注册一个新的块类型。
  • title:块的标题,显示在编辑器中。
  • icon:块的图标。
  • category:块所属的类别。
  • edit:编辑界面,使用InnerBlocks组件渲染子块。
  • save:保存界面,使用InnerBlocks.Content组件渲染子块。
  • useBlockProps:获取默认的块属性, 并传递 className 属性
  1. 解释关键部分:
  • edit函数中,<InnerBlocks />组件负责渲染子块的编辑界面。当用户在这个容器块中添加新的块时,它们会出现在这个区域。
  • save函数中,<InnerBlocks.Content />组件负责渲染子块的保存界面。它会将子块的内容保存到文章内容中。

第三幕:控制允许的子块类型

仅仅允许添加任何类型的块是不够的,有时候我们需要限制用户只能添加特定类型的块。比如,在我们的卡片容器中,我们可能只想允许添加标题、内容和按钮块。

我们可以使用allowedBlocks属性来控制允许的子块类型。

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

registerBlockType('my-plugin/card-container', {
  title: '卡片容器',
  icon: 'align-wide',
  category: 'common',
  attributes: {
    backgroundColor: {
      type: 'string',
      default: '#f0f0f0',
    },
  },
  edit: ({ className, attributes, setAttributes }) => {
    const { backgroundColor } = attributes;

    const onChangeBackgroundColor = (newColor) => {
      setAttributes({ backgroundColor: newColor });
    };

    const allowedBlocks = ['core/heading', 'core/paragraph', 'core/button'];

    const blockProps = useBlockProps({
      className: className,
      style: { backgroundColor: backgroundColor },
    });

    return (
      <div { ...blockProps }>
        <h2>卡片容器</h2>
        <div className="card-content">
        <InnerBlocks allowedBlocks={allowedBlocks} />
        </div>
      </div>
    );
  },
  save: ({ className, attributes }) => {
    const { backgroundColor } = attributes;

    const blockProps = useBlockProps.save({
      className: className,
      style: { backgroundColor: backgroundColor },
    });

    return (
      <div { ...blockProps }>
        <div className="card-content">
        <InnerBlocks.Content />
        </div>
      </div>
    );
  },
});

在这个例子中,allowedBlocks属性被设置为一个包含'core/heading''core/paragraph''core/button'的数组。这意味着用户只能在这个卡片容器中添加标题、段落和按钮块。

第四幕:模板(Templates):预定义子块结构

有时候,我们希望在用户添加容器块时,自动包含一些预定义的子块。例如,我们希望卡片容器默认包含一个标题和一个段落。

我们可以使用template属性来实现这个功能。

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

registerBlockType('my-plugin/card-container-with-template', {
  title: '卡片容器(带模板)',
  icon: 'align-wide',
  category: 'common',
  edit: ({ className }) => {
    const template = [
      ['core/heading', { placeholder: '输入标题' }],
      ['core/paragraph', { placeholder: '输入内容' }],
    ];

    return (
      <div { ...useBlockProps({
        className: className,
      }) }>
        <h2>卡片容器(带模板)</h2>
        <InnerBlocks template={template} />
      </div>
    );
  },
  save: ({ className }) => {
    return (
      <div { ...useBlockProps.save({
        className: className,
      }) }>
        <h2>卡片容器(带模板)</h2>
        <InnerBlocks.Content />
      </div>
    );
  },
});

在这个例子中,template属性被设置为一个包含两个元素的数组。每个元素都是一个数组,第一个元素是块的名称,第二个元素是块的属性。

  • ['core/heading', { placeholder: '输入标题' }]:创建一个标题块,并设置其placeholder属性为“输入标题”。
  • ['core/paragraph', { placeholder: '输入内容' }]:创建一个段落块,并设置其placeholder属性为“输入内容”。

当用户添加这个卡片容器块时,它会自动包含一个标题和一个段落,并且标题和段落的placeholder属性会被设置为指定的值。

第五幕:锁住你的块(Block Locking):控制用户操作

有时候,我们希望限制用户对子块的操作,比如禁止删除或移动它们。我们可以使用templateLock属性来实现这个功能。

templateLock属性有三个可能的值:

  • false:允许用户自由操作子块。
  • 'all':禁止用户删除、移动或添加子块。
  • 'insert':只允许用户添加子块,禁止删除和移动。
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

registerBlockType('my-plugin/card-container-with-lock', {
  title: '卡片容器(带锁定)',
  icon: 'align-wide',
  category: 'common',
  edit: ({ className }) => {
    const template = [
      ['core/heading', { placeholder: '输入标题' }],
      ['core/paragraph', { placeholder: '输入内容' }],
    ];

    return (
      <div { ...useBlockProps({
        className: className,
      }) }>
        <h2>卡片容器(带锁定)</h2>
        <InnerBlocks template={template} templateLock="all" />
      </div>
    );
  },
  save: ({ className }) => {
    return (
      <div { ...useBlockProps.save({
        className: className,
      }) }>
        <h2>卡片容器(带锁定)</h2>
        <InnerBlocks.Content />
      </div>
    );
  },
});

在这个例子中,templateLock属性被设置为'all'。这意味着用户无法删除、移动或添加子块。

第六幕:深入挖掘:useInnerBlocksProps Hook

useInnerBlocksProps 是一个 React Hook,它提供了一种更灵活的方式来配置 InnerBlocks 组件的行为。它返回一个包含 props 的对象,你可以将这些 props 传递给你的自定义组件,从而实现对子块渲染的更精细控制。

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks, useInnerBlocksProps } from '@wordpress/block-editor';

registerBlockType('my-plugin/custom-container', {
  title: '自定义容器',
  icon: 'align-wide',
  category: 'common',
  edit: ({ className }) => {

    const innerBlocksProps = useInnerBlocksProps({
      className: 'my-custom-inner-blocks',
    });

    return (
      <div { ...useBlockProps({
        className: className,
      }) }>
        <h2>自定义容器</h2>
        <div { ...innerBlocksProps } />
      </div>
    );
  },
  save: ({ className }) => {
    const innerBlocksProps = useInnerBlocksProps.save({});

    return (
      <div { ...useBlockProps.save({
        className: className,
      }) }>
        <h2>自定义容器</h2>
        <div { ...innerBlocksProps } />
      </div>
    );
  },
});

在这个例子中,我们使用了 useInnerBlocksProps hook 来获取 props,然后将这些 props 传递给一个自定义的 div 元素。这允许我们为子块的渲染区域添加自定义的 class name (my-custom-inner-blocks),从而可以更灵活地控制样式。

useInnerBlocksProps 的常见用法:

  • 自定义容器样式: 通过添加自定义的 class name 或 style,可以改变子块渲染区域的外观。
  • 处理子块的事件: 可以监听子块的事件,例如添加、删除或移动事件,并执行相应的操作。
  • 与其他组件集成: 可以将 useInnerBlocksProps 与其他 React 组件集成,从而实现更复杂的布局和交互。

第七幕:高级技巧:动态渲染子块

在某些情况下,我们可能需要根据父块的属性来动态渲染子块。例如,我们可能希望创建一个“标签页”块,用户可以添加任意数量的标签页,每个标签页都包含一个标题和一个内容区域。

要实现这个功能,我们需要使用JavaScript来动态生成子块的配置。

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InnerBlocks, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl } from '@wordpress/components';

registerBlockType('my-plugin/tab-container', {
  title: '标签页容器',
  icon: 'align-wide',
  category: 'common',
  attributes: {
    numberOfTabs: {
      type: 'number',
      default: 2,
    },
  },
  edit: ({ attributes, setAttributes }) => {
    const { numberOfTabs } = attributes;

    const onChangeNumberOfTabs = (newNumberOfTabs) => {
      setAttributes({ numberOfTabs: newNumberOfTabs });
    };

    const template = Array(numberOfTabs)
      .fill()
      .map((_, index) => ['my-plugin/tab-item', { title: `标签页 ${index + 1}` }]);

    return (
      <>
        <InspectorControls>
          <PanelBody title="标签页设置">
            <RangeControl
              label="标签页数量"
              value={numberOfTabs}
              onChange={onChangeNumberOfTabs}
              min={1}
              max={5}
            />
          </PanelBody>
        </InspectorControls>
        <div { ...useBlockProps() }>
          <h2>标签页容器</h2>
          <InnerBlocks template={template} allowedBlocks={['my-plugin/tab-item']} templateLock="insert" />
        </div>
      </>
    );
  },
  save: ({ attributes }) => {
    return (
      <div { ...useBlockProps.save() }>
        <InnerBlocks.Content />
      </div>
    );
  },
});

registerBlockType('my-plugin/tab-item', {
  title: '标签页',
  icon: 'align-wide',
  category: 'common',
  parent: ['my-plugin/tab-container'],
  attributes: {
    title: {
      type: 'string',
      default: '标签页',
    },
  },
  edit: ({ attributes, setAttributes }) => {
    const { title } = attributes;

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

    return (
      <div { ...useBlockProps() }>
        <h3>{title}</h3>
        <InnerBlocks />
      </div>
    );
  },
  save: ({ attributes }) => {
    const { title } = attributes;
    return (
      <div { ...useBlockProps.save() }>
        <h3>{title}</h3>
        <InnerBlocks.Content />
      </div>
    );
  },
});

在这个例子中:

  1. 父块(my-plugin/tab-container: 允许用户控制标签页的数量。
  2. 子块(my-plugin/tab-item: 表示一个单独的标签页,包含一个标题和一个内容区域。
  3. 动态模板: 父块的 edit 函数使用 numberOfTabs 属性来动态生成 template 数组。
  4. InspectorControls: 允许用户在侧边栏调整标签页的数量。
  5. parent属性: 子块的 parent 属性限制它只能被添加到 my-plugin/tab-container 块中。

第八幕:常见问题与注意事项

  • 性能问题: 过度使用嵌套块可能会导致性能问题,特别是当嵌套层级很深时。尽量避免不必要的嵌套,并优化你的块代码。
  • 用户体验: 复杂的嵌套结构可能会让用户感到困惑。确保你的块设计简单易懂,并提供清晰的视觉提示。
  • 兼容性: 确保你的块与不同的主题和插件兼容。测试你的块在各种环境下是否正常工作。
  • clientId 的重要性: 每个块都有一个唯一的 clientId,它用于在编辑器中标识块。在处理嵌套块时,clientId 非常重要,因为它可以帮助你找到特定的子块。
  • 更新子块属性: 如果你需要在父块中更新子块的属性,可以使用 wp.data API 来 dispatch 一个 action。

第九幕:总结与展望

InnerBlocks是古腾堡编辑器中一个非常强大的工具,它允许你创建复杂的、可复用的块结构。通过灵活运用allowedBlockstemplatetemplateLock属性,你可以构建各种各样的嵌套块,满足不同的需求。

当然,InnerBlocks 还有很多高级用法等待你去探索。例如,你可以使用自定义的渲染函数来控制子块的显示方式,或者使用服务端渲染来优化性能。

希望今天的分享对你有所帮助。记住,编程的乐趣在于不断学习和实践。祝你玩得开心!

最后的彩蛋:一些实用技巧

技巧 说明
使用 block.json 文件 将块的注册信息放在 block.json 文件中,可以提高代码的可维护性。
使用 useEntityProp hook 这个 hook 可以让你轻松地访问和更新文章的元数据,例如文章的标题、内容和自定义字段。
使用 withSelectwithDispatch 这两个高阶组件可以让你将 WordPress 数据存储中的数据和 action 映射到你的块组件中。
善用控制台调试 在开发过程中,使用浏览器的控制台可以帮助你调试代码,查看错误信息和变量的值。

好了,今天的分享就到这里。希望大家有所收获,下次再见!

发表回复

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