### 验证常见问题解答

**区块如何变为无效状态？**

导致区块失效最常见的两种原因是：

1. 区块代码存在缺陷，导致内容被意外修改。插件开发者可参阅下文了解如何调试区块失效问题。
2. 您或外部编辑器对区块的HTML标记进行了更改，导致其不再符合规范。

**作为插件开发者，应如何调试区块被标记为无效的问题？**

开始调试前，请务必先了解上文所述的验证步骤，该步骤记录了检测区块是否无效的流程。当区块重新生成的标记与文章内容中保存的标记不匹配时，该区块即被视为无效，这通常是由于区块属性未能从已保存内容中正确解析所致。

若您正在使用[属性源](/docs/reference-guides/block-api/block-attributes.md)，请确保从标记中提取的属性完全符合预期，且类型正确（通常应为`'字符串'`或`'数字'`类型）。

当检测到区块无效时，浏览器开发者工具控制台会记录警告信息。该警告会包含标记差异具体位置的详细信息。请仔细比对预期标记与实际标记的差异，定位问题根源。

**更改区块的`save`行为后，旧内容中出现无效区块应如何修复？**

请参阅[过时区块处理指南](/docs/reference-guides/block-api/block-deprecation.md)，了解如何在主动调整标记时兼容历史内容。

## 示例

以下是结合使用属性、编辑和保存功能的几个示例。

### 将属性保存至子元素

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

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

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

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

### 通过序列化保存属性

理想情况下，已保存的属性应包含在标记中。但有时这并不现实，因此如果未指定属性来源，该属性会被序列化并保存到块的注释分隔符中。

此示例适用于动态块（例如[最新文章块](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-library/src/latest-posts/index.js)），其标记在服务器端渲染。虽然仍需保存函数，但此时仅返回 null，因为该块并不保存编辑器中的内容。

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

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

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

save: () => {
	return null;
}
```

## 验证

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

如果检测到某个块无效，系统将提示用户选择如何处理无效情况：

![无效块提示](https://user-images.githubusercontent.com/7753001/88754471-4cf7e900-d191-11ea-9123-3cee20719d10.png)

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

点击块侧边的"三点"菜单会显示三个选项：

-   **解决**：打开解决块对话框，其中包含两个按钮：
    -   **转换为 HTML**：保护已保存文章内容中的原始标记，并将块从其原始类型转换为 HTML 块类型，使用户能够直接修改 HTML 标记。
    -   **转换为块**：保护已保存文章内容中的原始标记，并将块从其原始类型转换为经过验证的块类型。
-   **转换为 HTML**：保护已保存文章内容中的原始标记，并将块从其原始类型转换为 HTML 块类型，使用户能够直接修改 HTML 标记。
-   **转换为经典块**：将已保存文章内容中的原始标记视为正确。由于块将从其原始类型转换为经典块类型，因此无法再使用原始块类型的控件来编辑内容。

## 保存函数

`save` 函数定义了如何将不同属性组合成最终标记，随后将其序列化到 `post_content` 中。

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

	return <div { ...blockProps }> 您的区块内容 </div>;
};
```

对于大多数区块而言，`save` 的返回值应是一个 [WordPress 元素实例](/packages/element/README.md)，用于表征区块在前端站点的呈现效果。

_注意：_ 虽然可以从 `save` 返回字符串值，但该值*会被转义处理*。若字符串包含 HTML 标记，这些标记将在前端站点以字面形式显示，而不会解析为等效的 HTML 节点内容。如必须从 `save` 返回原始 HTML，请使用 `wp.element.RawHTML`。但正如其名所示，这种方式易受[跨站脚本攻击](https://en.wikipedia.org/wiki/Cross-site_scripting)，因此建议尽可能使用 WordPress 元素层级结构。

_注意：_ 保存函数应是纯函数且无状态，仅依赖于调用时传入的属性。它不应使用任何类似 `useState` 或 `useEffect` 的 API，也不应从其他来源获取信息；例如，无法在函数内部使用数据模块 - `select( store ).selector( ... )`。
这是因为当外部信息发生变化时，后续编辑文章时可能会导致区块被标记为无效状态（[了解更多验证信息](#validation)）。

若需要将其他信息纳入保存过程，开发者可考虑以下两种方案：

-   使用[动态区块](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md)并在服务器端动态获取所需信息
-   将外部值存储为属性，并在区块的 `edit` 函数中随变更动态更新

对于[动态区块](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md)，`save` 的返回值可表征区块内容的缓存副本，仅当实现区块的插件被禁用时显示。

若未明确指定，默认实现将不会在文章内容中保存动态区块的标记，而是始终在区块前端显示时实时计算。

### 区块包装器属性

与 `edit` 函数类似，在渲染静态区块时，必须将 `useBlockProps.save()` 返回的区块属性添加到区块的包装元素。这能确保区块类名正确渲染，同时包含由区块支持 API 注入的所有 HTML 属性。

### 属性参数

与 `edit` 相同，`save` 函数也会接收包含属性的对象参数，这些属性可插入到标记中。

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

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

保存区块时，需按照属性源定义规定的相同格式保存属性。若未指定属性源，属性将保存至区块的注释分隔符。详见[区块属性文档](/docs/reference-guides/block-api/block-attributes.md)获取更多信息。

### 内部区块

传递给 `save` 函数的参数中还存在第二个属性 `innerBlocks`。该属性通常用于内部操作，实际需要使用的情况极为罕见。

初始化时，`innerBlocks` 是一个包含嵌套区块对象表征的数组。在极少数需要使用此属性的场景中，它可帮助您调整区块的渲染方式。例如，您可以根据嵌套区块的数量或特定区块类型的存在情况来差异化渲染区块。

```jsx
save: ( { attributes, innerBlocks } ) => {
	const { 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>;
};
```

此示例中，当内部区块数量超过一个时，会为区块添加额外类名，从而实现区块的差异化样式设计。

# 编辑与保存

当在客户端使用 JavaScript 注册区块时，`edit` 和 `save` 函数定义了区块在编辑器中的渲染方式、操作逻辑以及保存机制。

## 编辑函数

`edit` 函数描述了区块在编辑器上下文中的结构，决定了编辑器在使用该区块时的渲染内容。

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

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

	// ...

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

		return <div { ...blockProps }>您的区块内容</div>;
	},
};
```

### 区块包装器属性

首先需要注意的是在区块包装元素上使用的 `useBlockProps` React 钩子。上例中区块包装器在编辑器内渲染为「div」元素，但为了让 Gutenberg 编辑器能够操作区块、添加必要的额外类名...区块包装元素必须应用从 `useBlockProps` React 钩子调用中获取的属性。区块包装元素应是原生 DOM 元素（如 `<div>` 和 `<table>`），或是能将附加属性传递至原生 DOM 元素的 React 组件。而使用 `<Fragment>` 或 `<ServerSideRender>` 等组件则不符合要求。

若包装元素需要自定义 HTML 属性，需将其作为参数传递给 `useBlockProps` 钩子。例如要为包装器添加 `my-random-classname` 类名：

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

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

	// ...

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

		return <div { ...blockProps }>您的区块内容</div>;
	},
};
```

### 属性参数

`edit` 函数通过对象参数接收多个属性，可用于调整区块行为。

`attributes` 属性会呈现所有可用属性及其对应值，这些属性在区块类型注册时通过 `attributes` 属性定义（详见[属性文档](/docs/reference-guides/block-api/block-attributes.md)）。假设在区块注册时定义了 `content` 属性，即可在编辑函数中调用该值：

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

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

当区块插入编辑器时，`attributes.content` 的值将显示在 `div` 中。

### 选中状态

isSelected 属性为布尔值，用于标识区块当前是否被选中。

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

	return (
		<div { ...blockProps }>
			您的区块内容
			{ isSelected && (
				<span>仅当区块被选中时显示</span>
			) }
		</div>
	);
};
```

### 属性设置

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

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

	// 简化属性访问
	const { content, mySetting } = attributes;

	// 用户点击按钮时切换设置
	const toggleSetting = () => setAttributes( { mySetting: ! mySetting } );
	return (
		<div { ...blockProps }>
			{ content }
			{ isSelected && (
				<button onClick={ toggleSetting }>切换设置</button>
			) }
		</div>
	);
};
```

使用对象或数组类型的属性时，建议在更新前先进行复制或克隆：

```js
// 正确做法 - 基于旧列表属性创建新数组并添加新项：
const { list } = attributes;
const addListItem = ( newListItem ) =>
	setAttributes( { list: [ ...list, newListItem ] } );

// 错误做法 - 直接修改现有属性中的列表：
const { list } = attributes;
const addListItem = ( newListItem ) => {
	list.push( newListItem );
	setAttributes( { list } );
};
```

这样做的原因在于：JavaScript 中数组和对象通过引用传递，此做法可确保修改不会影响其他持有相同数据引用的代码。此外，Gutenberg 项目遵循 Redux 库的[状态不可变原则](https://redux.js.org/faq/immutable-data#what-are-the-benefits-of-immutability)——不应直接修改数据，而是创建包含变更的新数据版本。

`setAttribute` 也支持以更新函数作为参数。该函数必须是纯函数，接收当前属性作为唯一参数并返回更新后的属性。此方法适用于需要基于先前状态更新值，或处理对象和数组的场景。

_**注意：** 自 WordPress 6.9 起支持。_

```js
// 用户点击按钮时切换设置
const toggleSetting = () =>
	setAttributes( ( currentAttr ) => ( {
		mySetting: ! currentAttr.mySetting,
	} ) );

// 向列表添加项目
const addListItem = ( newListItem ) =>
	setAttributes( ( currentAttr ) => ( {
		list: [ ...currentAttr.list, newListItem ],
	} ) );
```