当使用 JavaScript 在客户端注册块时,edit 和 save 函数提供了块在编辑器中如何渲染、如何操作和被操纵以及如何保存的接口。

Edit(编辑)

edit 函数描述了您的块在编辑器上下文中的结构。这代表了当使用块时编辑器将渲染的内容。

import { useBlockProps } from '@wordpress/block-editor';

// ...
const blockSettings = {
    apiVersion: 3,

    // ...

    edit: () => {
        const blockProps = useBlockProps();

        return <div { ...blockProps }>Your block.</div>;
    },
};

块包装器属性(Block wrapper props)

这里首先要注意的是在块包装器元素上使用 useBlockProps React 钩子。在上述示例中,块包装器在编辑器中渲染一个“div”,但为了让 Gutenberg 编辑器知道如何操纵块、添加块所需的任何额外类名……块包装器元素应应用从 useBlockProps react 钩子调用中获取的属性。块包装器元素应该是原生 DOM 元素,如 <div> 和 <table>,或者是将任何额外属性转发给原生 DOM 元素的 React 组件。使用 <Fragment> 或 <ServerSideRender> 组件等将是无效的。

如果元素包装器需要任何额外的自定义 HTML 属性,这些需要通过参数传递给 useBlockProps 钩子。例如,要向包装器添加一个 my-random-classname className,您可以使用以下代码:

import { useBlockProps } from '@wordpress/block-editor';

// ...
const blockSettings = {
    apiVersion: 3,

    // ...

    edit: () => {
        const blockProps = useBlockProps( {
            className: 'my-random-classname',
        } );

        return <div { ...blockProps }>Your block.</div>;
    },
};

attributes(属性)

edit 函数还通过对象参数接收一些属性。您可以使用这些属性来调整块的行为。

attributes 属性显示了所有可用属性及其对应的值,正如在注册块类型时由 attributes 属性描述的那样。有关如何指定属性来源,请参阅属性文档。

在这种情况下,假设我们在块注册期间定义了一个名为 content 的属性,我们将在我们的 edit 函数中接收并使用该值:

edit: ( { attributes } ) => {
    const blockProps = useBlockProps();

    return <div { ...blockProps }>{ attributes.content }</div>;
};

当在编辑器中插入块时,attributes.content 的值将显示在 div 内部。

isSelected(是否选中)

isSelected 属性是一个布尔值,用于传达块当前是否被选中。

edit: ( { attributes, isSelected } ) => {
    const blockProps = useBlockProps();

    return (
        <div { ...blockProps }>
            Your block.
            { isSelected && (
                <span>Shows only when the block is selected.</span>
            ) }
        </div>
    );
};

setAttributes(设置属性)

此函数允许块根据用户交互更新单个属性。

edit: ( { attributes, setAttributes, isSelected } ) => {
    const blockProps = useBlockProps();

    // Simplify access to attributes
    const { content, mySetting } = attributes;

    // Toggle a setting when the user clicks the button
    const toggleSetting = () => setAttributes( { mySetting: ! mySetting } );
    return (
        <div { ...blockProps }>
            { content }
            { isSelected && (
                <button onClick={ toggleSetting }>Toggle setting</button>
            ) }
        </div>
    );
};

当使用对象或数组属性时,在更新之前最好复制或克隆该属性:

// Good - a new array is created from the old list attribute and a new list item:
const { list } = attributes;
const addListItem = ( newListItem ) =>
    setAttributes( { list: [ ...list, newListItem ] } );

// Bad - the list from the existing attribute is modified directly to add the new list item:
const { list } = attributes;
const addListItem = ( newListItem ) => {
    list.push( newListItem );
    setAttributes( { list } );
};

为什么要这样做?在 JavaScript 中,数组和对象是按引用传递的,因此这种实践可以确保更改不会影响可能持有相同数据引用的其他代码。此外,Gutenberg 项目遵循 Redux 库的哲学,即状态应该是不可变的——不应直接更改数据,而是创建一个包含更改的新版本数据。

setAttributes 还支持将更新器函数作为参数。它必须是一个纯函数,仅以当前属性为唯一参数并返回更新的属性。此方法在想要基于先前状态更新值或处理对象和数组时非常有用。

注意:自 WordPress 6.9 起。

// Toggle a setting when the user clicks the button.
const toggleSetting = () =>
    setAttributes( ( currentAttr ) => ( {
        mySetting: ! currentAttr.mySetting,
    } ) );

// Add item to the list.
const addListItem = ( newListItem ) =>
    setAttributes( ( currentAttr ) => ( {
        list: [ ...currentAttr.list, newListItem ],
    } ) );

Save(保存)

save 函数定义了如何将不同的属性组合成最终的标记,然后将其序列化为 post_content。

save: () => {
    const blockProps = useBlockProps.save();

    return <div { ...blockProps }> Your block. </div>;
};

对于大多数块,save 的返回值应是一个WordPress Element 实例,表示块在站点前端的外观。

注意:虽然可以从 save 返回字符串值,但它将被转义。如果字符串包含 HTML 标记,标记将以字面形式显示在站点前端,而不是作为等效的 HTML 节点内容。如果您必须从 save 返回原始 HTML,请使用 wp.element.RawHTML。顾名思义,这容易受到跨站脚本攻击的影响,因此只要可能,就建议使用 WordPress Element 层次结构而不是它。

注意:保存函数应是一个纯且无状态函数,仅依赖于调用它的属性。它不应使用任何 API,如 useState 或 useEffect,也不应从其他来源检索信息;例如,无法在内部使用 data 模块 – select( store ).selector( ... )。
这是因为如果外部信息发生变化,当稍后编辑帖子时,块可能会被标记为无效(阅读有关验证的更多信息)。

如果需要将其他信息作为保存的一部分,开发人员可以考虑以下两种替代方案:

  • 使用动态块并在服务器上动态检索所需的信息。
  • 将外部值存储为属性,该属性在块的 edit 函数中随着更改的发生而动态更新。

对于动态块,save 的返回值可以表示块的缓存内容副本,仅在实现该块的插件被禁用时显示。

如果未指定,默认实现将为动态块在帖子内容中保存无标记,而是将此操作推迟到块在站点前端显示时始终计算。

块包装器属性(block wrapper props)

与 edit 函数一样,在渲染静态块时,重要的是将 useBlockProps.save() 返回的块属性添加到块的包装器元素。这确保了除了由块支持 API 注入的任何 HTML 属性外,块类名也能正确渲染。

attributes(属性)

与 edit 一样,save 函数还接收一个包含属性的对象参数,这些属性可以插入到标记中。

save: ( { attributes } ) => {
    const blockProps = useBlockProps.save();

    return <div { ...blockProps }>{ attributes.content }</div>;
};

保存您的块时,您希望以属性源定义中指定的相同格式保存属性。如果未指定属性源,则属性将保存到块的注释分隔符中。有关更多详细信息,请参阅 块属性文档。

innerBlocks

传递给 save 函数的 props 中还有一个属性 innerBlocks。该属性通常用于内部操作,只有在极少数情况下才需要使用它。

innerBlocks 在初始化时是一个包含嵌套块的对象表示的数组。在那些可能使用此属性的罕见情况下,
它可以帮助您调整块的渲染方式。例如,您可以根据嵌套块的数量或是否存在特定块类型来以不同方式渲染块。

save: ( { attributes, innerBlocks } ) => {
    let { className, ...rest } = useBlockProps.save();

    // innerBlocks 在初始化时也可能是对象 - react 元素
    const numberOfInnerBlocks = innerBlocks?.length;
    if ( numberOfInnerBlocks > 1 ) {
        className = className + ( className ? ' ' : '' ) + 'more-than-one';
    };
    const blockProps =  { ...rest, className };

    return <div { ...blockProps }>{ attributes.content }</div>;
};

在此处,如果内联块的数量大于一个,则会给块添加额外的类名,从而允许以不同方式对块进行样式设置。

示例

以下是将属性、edit 和 save 一起使用的几个示例。

将属性保存到子元素

attributes: {
    content: {
        type: 'string',
        source: 'html',
        selector: 'div'
    }
},

edit: ( { attributes, setAttributes } ) => {
    const blockProps = useBlockProps();
    const updateFieldValue = ( val ) => {
        setAttributes( { content: val } );
    }
    return (
        <div { ...blockProps }>
            <TextControl
                label='我的文本字段'
                value={ attributes.content }
                onChange={ updateFieldValue }
            />
        </div>
    );
},

save: ( { attributes } ) => {
    const blockProps = useBlockProps.save();

    return <div { ...blockProps }> { attributes.content } </div>;
},

通过序列化保存属性

理想情况下,应保存的属性应包含在标记中。但是,有时这并不切实际,因此如果未指定属性源,则属性将被序列化为保存到块的注释分隔符中。

此示例可能是针对动态块(例如 最新文章块),该块在服务器端渲染标记。仍然需要保存函数,但在这种情况下,它仅返回 null,因为该块不会从编辑器保存内容。

attributes: {
    postsToShow: {
        type: 'number',
    }
},

edit: ( { attributes, setAttributes } ) => {
    const blockProps = useBlockProps();

    return (
        <div { ...blockProps }>
            <TextControl
                label='显示文章数量'
                value={ attributes.postsToShow }
                onChange={ ( val ) => {
                    setAttributes( { postsToShow: parseInt( val ) } );
                }}
            />
        </div>
    );
},

save: () => {
    return null;
}

验证

当编辑器加载时,会验证帖子内容中的所有块以确定其准确性,以防止内容丢失。这与块的保存实现密切相关,因为如果编辑器无法正确恢复块,用户可能会无意中删除或修改其内容。在编辑器初始化期间,使用从帖子内容解析的属性为每个块重新生成保存的标记。如果新生成的标记与已存储在帖子内容中的标记不匹配,则会将该块标记为无效。这是因为我们假设除非用户进行编辑,否则标记应与保存的内容保持完全相同。

如果检测到块无效,将提示用户选择如何处理无效化:

无效块提示

点击 尝试恢复块 按钮将尽可能尝试恢复操作。

点击块侧面的“三点”菜单会显示三个选项:

  • 解决:打开“解决块”对话框,其中有两个按钮:
    • 转换为 HTML:保护来自保存帖子内容的原始标记,并将块从其原始类型转换为 HTML 块类型,使用户能够直接修改 HTML 标记。
    • 转换为块:保护来自保存帖子内容的原始标记,并将块从其原始类型转换为验证后的块类型。
  • 转换为 HTML:保护来自保存帖子内容的原始标记,并将块从其原始类型转换为 HTML 块类型,使用户能够直接修改 HTML 标记。
  • 转换为经典块:正确保护来自保存帖子内容的原始标记。由于块将从其原始类型转换为经典块类型,因此将无法再使用原始块类型可用的控件编辑内容。

验证常见问题解答

块如何变为无效?

导致块无效的两种最常见来源是:

  1. 块代码中的缺陷会导致意外修改内容。请参阅下面关于插件作者如何调试块无效化的问题。
  2. 您或外部编辑器以不再被视为正确的方式更改了块的 HTML 标记。

我是插件作者。我应该怎么做才能调试为什么我的块被标记为无效?

在开始调试之前,请务必熟悉上述描述验证步骤的内容,该步骤记录了检测块是否无效的过程。如果块的重新生成的标记与帖子内容中保存的内容不匹配,则该块无效,因此这通常可能是由于从保存内容中解析的属性不正确所致。

如果您使用 属性源,请确保从标记获取的属性以您预期的方式保存,并且类型正确(通常是 'string' 或 'number')。

当检测到块无效时,警告将记录到您的浏览器开发者工具控制台中。该警告将包含有关标记差异发生确切位置的详细信息。请务必仔细查看预期和实际标记之间的任何差异,以查看问题出在哪里。

我更改了块的 save 行为,旧内容现在包含无效块。如何解决这个问题?

参考关于 已弃用的块 的指南,以了解如何在故意的标记更改中容纳遗留内容。