## 服务器端函数

Interactivity API 提供了一系列便捷函数，使您能够在服务器端初始化和引用配置选项。这对于提供初始数据至关重要，服务器端指令处理将利用这些数据在 HTML 标记发送到浏览器前对其进行修改。这也是充分利用 WordPress 诸多 API（如随机数验证、AJAX 和翻译功能）的绝佳方式。

### wp_interactivity_config

`wp_interactivity_config` 用于设置或获取与存储命名空间关联的配置数组。
该配置在客户端也可用，但属于静态信息。

可将其视为网站交互的全局设置，这些设置不会随用户交互而更新。

设置示例：

```php
	wp_interactivity_config( 'myPlugin', array( 'showLikeButton' => is_user_logged_in() ) );
```

获取示例：

```php
  wp_interactivity_config( 'myPlugin' );
```

此配置可在客户端获取：

```js
// view.js

const { showLikeButton } = getConfig();
```

### wp_interactivity_state

`wp_interactivity_state` 用于在服务器端初始化全局状态，该状态将用于处理服务器端指令，随后会与客户端定义的任何全局状态合并。

在服务器端初始化全局状态还允许您使用许多关键的 WordPress API，包括 [AJAX](https://developer.wordpress.org/plugins/javascript/ajax/) 或 [随机数验证](https://developer.wordpress.org/plugins/javascript/enqueuing/#nonce)。

`wp_interactivity_state` 函数接收两个参数：一个用作引用标识的命名空间字符串，以及一个包含值的关联数组。

以下是传递带有随机数的 WP 管理后台 AJAX 端点的示例：

```php
// render.php

wp_interactivity_state(
	'myPlugin',
	array(
		'ajaxUrl' => admin_url( 'admin-ajax.php' ),
		'nonce'   => wp_create_nonce( 'myPlugin_nonce' ),
	),
);
```

```js
// view.js

const { state } = store( 'myPlugin', {
	actions: {
		*doSomething() {
			try {
				const formData = new FormData();
				formData.append( 'action', 'do_something' );
				formData.append( '_ajax_nonce', state.nonce );

				const data = yield fetch( state.ajaxUrl, {
					method: 'POST',
					body: formData,
				} ).then( ( response ) => response.json() );
				console.log( '服务器数据！', data );
			} catch ( e ) {
				// 出现错误！
			}
		},
	},
} );
```

### wp_interactivity_process_directives

`wp_interactivity_process_directives` 在处理完指令后返回更新后的 HTML。

这是 Interactivity API 服务器端渲染部分的核心函数，且为公开函数，因此任何 HTML（无论是否为区块）都可以被处理。

以下代码：

```php
wp_interactivity_state( 'myPlugin', array( 'greeting' => 'Hello, World!' ) );
$html_content = '<div data-wp-text="myPlugin::state.greeting"></div>';
$processed_html = wp_interactivity_process_directives( $html_content );
echo $processed_html;
```

将输出：

```html
<div data-wp-text="myPlugin::state.greeting">Hello, World!</div>
```

### wp_interactivity_data_wp_context

`wp_interactivity_data_wp_context` 返回上下文指令的字符串化 JSON。
此函数是在服务器端渲染标记中输出 `data-wp-context` 属性的推荐方式。

```php

$my_context = array(
	'counter' => 0,
	'isOpen'  => true,
);
?>
<div
 <?php echo wp_interactivity_data_wp_context( $my_context ); ?>
>
</div>
```

将输出：

```html
<div data-wp-context='{"counter":0,"isOpen":true}'></div>
```

### 私有存储

特定的存储命名空间可被标记为私有，从而阻止其他命名空间访问其内容。实现方式是在 `store()` 调用中添加 `lock` 选项，如下例所示。这样，后续对同一锁定命名空间执行 `store()` 时会抛出错误，意味着该命名空间仅能在首次 `store()` 调用返回引用的位置被访问。这对于希望隐藏部分插件存储、避免被扩展者访问的开发者特别有用。

```js
const { state } = store(
	'myPlugin/private',
	{ state: { messages: [ '私有信息' ] } },
	{ lock: true }
);

// 以下调用会抛出错误！
store( 'myPlugin/private', {
	/* 存储部分 */
} );
```

同时存在解锁私有存储的方法：可通过传递字符串（而非布尔值）作为 `lock` 参数。后续对同一命名空间的 `store()` 调用可使用该字符串解锁内容。只有知晓字符串锁的代码才能解锁受保护的存储命名空间。这对于在多个 JS 模块中定义的复杂存储非常实用。

```js
const { state } = store(
	'myPlugin/private',
	{ state: { messages: [ '私有信息' ] } },
	{ lock: PRIVATE_LOCK }
);

// 以下调用可正常执行
store(
	'myPlugin/private',
	{
		/* 存储部分 */
	},
	{ lock: PRIVATE_LOCK }
);
```

### 存储客户端方法

除存储函数外，还提供若干方法供开发者访问存储函数中的数据：

-   getContext()
    -   getServerContext()
    -   getServerState()
-   getElement()

#### getContext()

获取由评估存储函数的元素继承的上下文。返回值取决于元素及调用 `getContext()` 函数所在的命名空间。该方法可接受可选命名空间参数，以获取特定交互区域的上下文。

```js
const context = getContext( '命名空间' );
```

-   `命名空间`（可选）：与交互区域命名空间匹配的字符串。若未提供，则获取当前交互区域的上下文。

```php
// render.php
<div data-wp-interactive="myPlugin" data-wp-context='{ "isOpen": false }'>
	<button data-wp-on--click="actions.log">记录</button>
</div>
```

```js
// 存储模块
import { store, getContext } from '@wordpress/interactivity';

store( 'myPlugin', {
	actions: {
		log: () => {
			const context = getContext();
			// 输出 "false"
			console.log( '上下文 => ', context.isOpen );

			// 使用命名空间参数
			const myPluginContext = getContext( 'myPlugin' );
			// 输出 "false"
			console.log( 'myPlugin isOpen => ', myPluginContext.isOpen );
		},
	},
} );
```

#### getServerContext()

此函数与 `getContext()` 类似，但存在两个关键差异：

1. 当调用 [`@wordpress/interactivity-router`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity-router/) 中的 [`actions.navigate()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity-router/#actions) 时，`getServerContext()` 返回的对象会同步更新。适用于需要根据通过 `actions.navigate()` 加载页面传来的**新**上下文更新区块上下文的情况。该新上下文会嵌入通过 `actions.navigate()` 加载页面的 HTML 中。
2. `getServerContext()` 返回的对象为只读。

服务器上下文不能直接在指令中使用，但可通过回调函数订阅其变更。

```js
const serverContext = getServerContext( '命名空间' );
```

-   `命名空间`（可选）：与交互区域命名空间匹配的字符串。若未提供，则获取当前交互区域的服务器上下文。

使用示例：

```js
store( 'myPlugin', {
	callbacks: {
		updateServerContext() {
			const context = getContext();
			const serverContext = getServerContext();
			// 用服务器传来的新值覆盖特定属性
			context.overridableProp = serverContext.overridableProp;
		},
	},
} );
```

#### 副作用

自动响应状态变化。通常由 `data-wp-watch` 或 `data-wp-init` 指令触发。

#### 派生状态

返回经过计算的状态版本。可同时访问 `state` 和 `context`。

```js
// view.js
const { state } = store( 'myPlugin', {
	state: {
		amount: 34,
		defaultCurrency: 'EUR',
		currencyExchange: {
			USD: 1.1,
			GBP: 0.85,
		},
		get amountInUSD() {
			return state.currencyExchange[ 'USD' ] * state.amount;
		},
		get amountInGBP() {
			return state.currencyExchange[ 'GBP' ] * state.amount;
		},
	},
} );
```

### 在回调中访问数据

**`store`** 包含所有存储属性，如 `state`、`actions` 或 `callbacks`。这些属性通过 `store()` 调用返回，因此可以通过解构方式访问：

```js
const { state, actions } = store( 'myPlugin', {
	// ...
} );
```

`store()` 函数可被多次调用，所有存储部分将被合并：

```js
store( 'myPlugin', {
	state: {
		someValue: 1,
	},
} );

const { state } = store( 'myPlugin', {
	actions: {
		someAction() {
			state.someValue; // = 1
		},
	},
} );
```

<div class="callout callout-info">
  所有具有相同命名空间的 <code>store()</code> 调用都会返回相同的引用，即相同的 <code>state</code>、<code>actions</code> 等，包含所有传入存储部分的合并结果。
</div>

-   要在操作、派生状态或副作用中访问上下文，可使用 `getContext` 函数
-   要访问引用，可使用 `getElement` 函数

```js
const { state } = store( 'myPlugin', {
	state: {
		get someDerivedValue() {
			const context = getContext();
			const { ref } = getElement();
			// ...
		},
	},
	actions: {
		someAction() {
			const context = getContext();
			const { ref } = getElement();
			// ...
		},
	},
	callbacks: {
		someEffect() {
			const context = getContext();
			const { ref } = getElement();
			// ...
		},
	},
} );
```

这种实现方式赋予指令灵活而强大的功能：

-   操作和副作用可读取并修改状态及上下文
-   区块中的操作和状态可被其他区块访问
-   操作和副作用能执行常规 JavaScript 函数的所有操作，如访问 DOM 或发起 API 请求
-   副作用会自动响应状态变化

### 设置存储

#### 客户端设置

开发者可在_每个区块的 `view.js` 文件_中定义状态和存储元素，引用操作、副作用或派生状态等功能。

用于在 JavaScript 中设置存储的 `store` 方法可从 `@wordpress/interactivity` 导入：

```js
// store
import { store, getContext } from '@wordpress/interactivity';

store( 'myPlugin', {
	actions: {
		toggle: () => {
			const context = getContext();
			context.isOpen = ! context.isOpen;
		},
	},
	callbacks: {
		logIsOpen: () => {
			const { isOpen } = getContext();
			// 每次 isOpen 变化时记录其值
			console.log( `Is open: ${ isOpen }` );
		},
	},
} );
```

#### 服务端设置

状态也可在服务端使用 `wp_interactivity_state()` 函数进行初始化。通常可在区块的 `render.php` 文件中实现（`render.php` 模板于 [WordPress 6.1](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) 引入）。

通过 `wp_interactivity_state()` 在服务端定义的状态会与 view.js 文件中定义的存储合并。

`wp_interactivity_state` 函数接收两个参数：作为引用标识的命名空间`字符串`，以及包含值的[关联数组](https://www.php.net/manual/en/language.types.array.php)。

_从服务端初始化存储的示例（`state` = `{ someValue: 123 }`）_

```php
// render.php
wp_interactivity_state( 'myPlugin', array (
	'someValue' => get_some_value()
));
```

在服务端初始化状态还允许使用任何 WordPress API。例如，可使用核心翻译 API 翻译部分状态内容：

```php
// render.php
wp_interactivity_state( 'favoriteMovies', array(
      "1" => array(
        "id" => "123-abc",
        "movieName" => __("someMovieName", "textdomain")
      ),
) );
```

### `wp-text` 指令

该指令用于设置 HTML 元素的内部文本内容。

```html
<div data-wp-context='{ "text": "文本1" }'>
	<span data-wp-text="context.text"></span>
	<button data-wp-on--click="actions.toggleContextText">
		切换上下文文本
	</button>
</div>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		toggleContextText: () => {
			const context = getContext();
			context.text = context.text === '文本1' ? '文本2' : '文本1';
		},
	},
} );
```

</details>

`wp-text` 指令会在以下时机执行：

-   元素创建时
-   每当涉及获取指令最终值的 `state` 或 `context` 属性发生变化时（在回调函数或作为引用传递的表达式中）

返回值将用于改变元素的内部内容：`<div>数值</div>`。

### `wp-on` 指令

<div class="callout callout-info">
  若您的指令代码无需同步访问事件对象，建议改用性能更优的 <a href="#wp-on-async"><code>wp-on-async</code></a>。如需同步访问，可考虑实现 <a href="#async-actions"><code>异步操作</code></a>，在调用同步 API 后释放主线程控制权。
</div>

该指令用于在触发 DOM 事件（如 `click` 或 `keyup`）时执行代码。语法格式为 `data-wp-on--[事件名]`（例如 `data-wp-on--click` 或 `data-wp-on--keyup`）。

```php
<button data-wp-on--click="actions.logTime" >
  点击我！
</button>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		logTime: ( event ) => {
			console.log( new Date() );
		},
	},
} );
```

</details>

每当关联事件触发时，`wp-on` 指令便会执行。

作为引用传递的回调函数会接收[事件对象](https://developer.mozilla.org/en-US/docs/Web/API/Event)（`event`），该回调函数的返回值将被忽略。

### `wp-on-async` 指令

这是 `wp-on` 指令的高性能版本。它会立即释放主线程控制权，避免造成长任务阻塞，让其他等待主线程的交互操作能更快执行。当不需要同步访问 `event` 对象（特别是 `event.preventDefault()`、`event.stopPropagation()` 和 `event.stopImmediatePropagation()` 方法）时，请使用此异步版本。

### `wp-on-window` 指令

<div class="callout callout-info">
  若您的指令代码无需同步访问事件对象，建议改用性能更优的 <a href="#wp-on-async-window"><code>wp-on-async-window</code></a>。如需同步访问，可考虑实现 <a href="#async-actions"><code>异步操作</code></a>，在调用同步 API 后释放主线程控制权。
</div>

该指令允许您绑定全局窗口事件（如 `resize`、`copy` 和 `focus`），并在事件触发时执行预定义的回调函数。

[支持的窗口事件列表](https://developer.mozilla.org/en-US/docs/Web/API/Window#events)

指令语法为 `data-wp-on-window--[窗口事件名]`（例如 `data-wp-on-window--resize` 或 `data-wp-on-window--languagechange`）。

```php
<div data-wp-on-window--resize="callbacks.logWidth"></div>
```

<details>
	<summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	callbacks: {
		logWidth() {
			console.log( '窗口宽度: ', window.innerWidth );
		},
	},
} );
```

</details>

作为引用传递的回调函数会接收[事件对象](https://developer.mozilla.org/en-US/docs/Web/API/Event)（`event`），该回调函数的返回值将被忽略。当元素从 DOM 中移除时，对应的事件监听器也会同步移除。

### `wp-on-async-window` 指令

与 `wp-on-async` 类似，这是 `wp-on-window` 的优化版本，会立即释放主线程控制权以避免长任务阻塞。当不需要同步访问 `event` 对象（特别是 `event.preventDefault()`、`event.stopPropagation()` 和 `event.stopImmediatePropagation()` 方法）时，请使用此异步版本。此事件监听器还会以[被动模式](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive)进行注册。

#### getServerState()

获取交互区域的服务器状态。

此函数与 `getServerContext()` 用途相同，但返回的是**状态**而非**上下文**。

返回的对象为只读，包含通过 PHP 中 `wp_interactivity_state()` 定义的状态。当使用 [`@wordpress/interactivity-router`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity-router/) 中的 [`actions.navigate()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity-router/#actions) 时，`getServerState()` 返回的对象会更新以反映其属性的变化，而不会影响 `store()` 返回的状态。指令可以订阅这些变化以在需要时更新状态。

```js
const serverState = getServerState( 'namespace' );
```

-   `namespace`（可选）：与交互区域命名空间匹配的字符串。如果未提供，则获取当前交互区域的服务器状态。

使用示例：

```js
const { state } = store( 'myStore', {
	callbacks: {
		updateServerState() {
			const serverState = getServerState();
			// 用服务器传来的新值覆盖某些属性。
			state.overridableProp = serverState.overridableProp;
		},
	},
} );
```

#### getElement()

获取绑定或调用操作的元素表示。该表示为只读，包含对 DOM 元素的引用、其属性及本地响应式状态。
返回一个包含两个键的对象：

##### ref

`ref` 是对 DOM 元素的引用，类型为 [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement)。相当于 Preact 或 React 中的 `useRef`，因此在 `ref` 尚未附加到实际 DOM 元素时（例如在水合或挂载期间）可能为 `null`。

##### attributes

`attributes` 包含一个 [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)，它添加了一个允许引用其他存储命名空间的 getter。欢迎在代码中查看该 getter。[链接](https://github.com/WordPress/gutenberg/blob/8cb23964d58f3ce5cf6ae1b6f967a4b8d4939a8e/packages/interactivity/src/store.ts#L70)

这些属性将包含该元素的指令。在按钮示例中：

```js
// store
import { store, getElement } from '@wordpress/interactivity';

store( 'myPlugin', {
	actions: {
		log: () => {
			const element = getElement();
			// 输出属性
			console.log( 'element attributes => ', element.attributes );
		},
	},
} );
```

代码将输出：

```json
{
	"data-wp-on--click": 'actions.log',
	"children": ['Log'],
	"onclick": event => { evaluate(entry, event); }
}
```

### withScope()

操作在被调用时可能依赖于作用域，例如调用 `getContext()` 或 `getElement()` 时。

当交互 API 运行时执行回调时，作用域会自动设置。但是，如果从不由运行时执行的回调（如 `setInterval()` 回调）中调用操作，则需要确保作用域正确设置。在这些情况下，使用 `withScope()` 函数确保作用域正确设置。

例如，在没有包装器的情况下，`actions.nextImage` 会触发未定义错误：

```js
store( 'mySliderPlugin', {
	callbacks: {
		initSlideShow: () => {
			setInterval(
				withScope( () => {
					actions.nextImage();
				} ),
				3_000
			);
		},
	},
} );
```

### withSyncEvent()

需要同步访问 `event` 对象的操作必须使用 `withSyncEvent()` 函数来注解其处理程序回调。这是由于正在进行的努力，默认情况下将存储操作视为异步处理，除非它们需要同步事件访问。因此，从 Gutenberg 20.4 / WordPress 6.8 开始，所有需要同步事件访问的操作都必须使用 `withSyncEvent()` 函数。否则将触发弃用警告，并在未来版本中相应更改行为。

只有非常特定的事件方法和属性需要同步访问，因此建议仅在必要时使用 `withSyncEvent()`。以下事件方法和属性需要同步访问：

* `event.currentTarget`
* `event.preventDefault()`
* `event.stopImmediatePropagation()`
* `event.stopPropagation()`

以下示例展示了一个操作需要同步事件访问，而其他操作不需要的情况：

```js
// store
import { store, withSyncEvent } from '@wordpress/interactivity';

store( 'myPlugin', {
	actions: {
		// `event.preventDefault()` 需要同步事件访问。
		preventNavigation: withSyncEvent( ( event ) => {
			event.preventDefault();
		} ),

		// `event.target` 不需要同步事件访问。
		logTarget: ( event ) => {
			console.log( 'event target => ', event.target );
		},

		// 完全不使用 `event` 不需要同步事件访问。
		logSomething: () => {
			console.log( 'something' );
		},
	},
} );
```

# API 参考

<div class="callout callout-alert">
交互性 API 仅适用于 WordPress 6.5 及以上版本。
</div>

要通过交互性 API 为区块添加交互功能，开发者可以使用：

-   **指令：** 添加到标记中，为区块的 DOM 元素添加特定行为
-   **存储：** 包含行为所需的逻辑和数据（状态、操作、副作用等）

DOM 元素通过指令与存储在状态和上下文中的数据建立连接。如果状态或上下文中的数据发生变化，指令将响应这些变化，并相应地更新 DOM（参见[示意图](https://excalidraw.com/#json=T4meh6lltJh6TCX51NTIu,DmIhxYSGFTL_ywZFbsmuSw)）。

![状态与指令](https://make.wordpress.org/core/files/2024/02/interactivity-state-directives.png)

## 什么是指令？

指令是自定义属性，可添加到区块标记中，为其 DOM 元素添加行为。这可以在 `render.php` 文件（针对动态区块）或 `save.js` 文件（针对静态区块）中完成。

交互性 API 指令使用 `data-` 前缀。以下是一个在 HTML 标记中使用指令的示例：

```html
<div
	data-wp-interactive="myPlugin"
	data-wp-context='{ "isOpen": false }'
	data-wp-watch="callbacks.logIsOpen"
>
	<button
		data-wp-on--click="actions.toggle"
		data-wp-bind--aria-expanded="context.isOpen"
		aria-controls="p-1"
	>
		切换
	</button>

	<p id="p-1" data-wp-bind--hidden="!context.isOpen">
		此元素现在可见！
	</p>
</div>
```

指令还可以使用 [HTML 标签处理器](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2)动态注入。

通过指令，可以直接管理交互，例如副作用、状态、事件处理程序、属性或内容。

## 指令列表

### `wp-interactive`

`wp-interactive` 指令通过交互性 API（指令和存储）为 DOM 元素及其子元素“激活”交互功能。该指令包含一个命名空间，用于引用特定的存储，可以设置为 `string` 或 `object`。

```html
<!-- 让此元素及其子元素具有交互性，并设置命名空间 -->
<div
	data-wp-interactive="myPlugin"
	data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
	<p>
		我现在具有交互性，
		<span data-wp-style--background-color="context.myBgColor"
			>并且可以使用指令！</span
		>
	</p>
	<div>
		<p>
		我也具有交互性，
			<span data-wp-style--color="context.myColor"
				>并且我也可以使用指令！</span
			>
		</p>
	</div>
</div>

<!-- 这也是有效的 -->
<div
	data-wp-interactive='{ "namespace": "myPlugin" }'
	data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
	<p>
		我现在具有交互性，
		<span data-wp-style--background-color="context.myBgColor"
			>并且可以使用指令！</span
		>
	</p>
	<div>
		<p>
		我也具有交互性，
			<span data-wp-style--color="context.myColor"
				>并且我也可以使用指令！</span
			>
		</p>
	</div>
</div>
```

<div class="callout callout-info">
  使用 <code>data-wp-interactive</code> 是交互性 API“引擎”工作的必要条件。在以下示例中，为了简化，未添加 <code>data-wp-interactive</code>。此外，<code>data-wp-interactive</code> 指令将在未来自动注入。
</div>

### `wp-context`

它提供了一个**局部**状态，可供特定的 HTML 节点及其子节点使用。

`wp-context` 指令接受一个字符串化的 JSON 作为值。

```php
// render.php
<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }' >
  <button data-wp-on--click="actions.logId" >
    点击我！
  </button>
</div>
```

<details>
  <summary><em>查看与上述指令一起使用的存储</em></summary>

```js
store( 'myPlugin', {
	actions: {
		logId: () => {
			const { post } = getContext();
			console.log( post.id );
		},
	},
} );
```

</details>

可以在不同层级定义不同的上下文，更深层级的上下文会将其自身的上下文与任何父级上下文合并：

```html
<div data-wp-context='{ "foo": "bar" }'>
	<span data-wp-text="context.foo"><!-- 将输出："bar" --></span>

	<div data-wp-context='{ "bar": "baz" }'>
		<span data-wp-text="context.foo"><!-- 将输出："bar" --></span>

		<div data-wp-context='{ "foo": "bob" }'>
			<span data-wp-text="context.foo"><!-- 将输出："bob" --></span>
		</div>
	</div>
</div>
```

### `wp-on-document`

<div class="callout callout-info">
  若您的指令代码无需同步访问事件对象，建议改用性能更佳的 <a href="#wp-on-async-document"><code>wp-on-async-document</code></a>。如需同步访问，可考虑实现一个<a href="#async-actions"><code>异步操作</code></a>，在调用同步 API 后主动让出主线程。
</div>

该指令允许您绑定全局文档事件（如 `scroll`、`mousemove` 和 `keydown`），并在事件触发时执行预定义的回调函数。

[支持的文档事件列表](https://developer.mozilla.org/en-US/docs/Web/API/Document#events)

指令语法为 `data-wp-on-document--[文档事件名]`（例如 `data-wp-on-document--keydown` 或 `data-wp-on-document--selectionchange`）。

```php
<div data-wp-on-document--keydown="callbacks.logKeydown"></div>
```

<details>
	<summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	callbacks: {
		logKeydown( event ) {
			console.log( '按键按下: ', event.key );
		},
	},
} );
```

</details>

传入的回调函数会接收[事件对象](https://developer.mozilla.org/en-US/docs/Web/API/Event)（`event`），其返回值将被忽略。当元素从 DOM 中移除时，对应的事件监听器也会同步移除。

### `wp-on-async-document`

与 `wp-on-async` 类似，这是 `wp-on-document` 的优化版本，会立即让出主线程以避免造成长任务。当不需要同步访问 `event` 对象（特别是 `event.preventDefault()`、`event.stopPropagation()` 和 `event.stopImmediatePropagation()` 方法）时，请使用此异步版本。此事件监听器还会被添加为[被动模式](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive)。

### `wp-watch`

该指令会在**节点创建时执行回调，并在状态或上下文变更时重新执行**。

您可以通过 `data-wp-watch--[唯一标识]` 语法为同一 DOM 元素附加多个副作用回调。

`唯一标识` 无需全局唯一，只需在该 DOM 元素的 `wp-watch` 指令中保持唯一性即可。

```html
<div data-wp-context='{ "counter": 0 }' data-wp-watch="callbacks.logCounter">
	<p>计数器: <span data-wp-text="context.counter"></span></p>
	<button data-wp-on--click="actions.increaseCounter">+</button>
	<button data-wp-on--click="actions.decreaseCounter">-</button>
</div>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		increaseCounter: () => {
			const context = getContext();
			context.counter++;
		},
		decreaseCounter: () => {
			const context = getContext();
			context.counter--;
		},
	},
	callbacks: {
		logCounter: () => {
			const { counter } = getContext();
			console.log( '计数器值：' + counter + ' 记录时间：' + new Date() );
		},
	},
} );
```

</details>

`wp-watch` 指令会在以下时机执行：

-   元素创建时
-   回调函数内部使用的 `state` 或 `context` 属性发生变更时

`wp-watch` 指令可返回一个函数。若返回函数，该函数将作为清理逻辑使用：在回调再次执行前运行，并在元素从 DOM 移除时再次执行。

典型使用场景包括：

-   日志记录
-   修改页面标题
-   通过 `.focus()` 设置元素焦点
-   满足特定条件时更新状态或上下文

### `wp-init`

该指令**仅在节点创建时执行一次回调**。

您可以通过 `data-wp-init--[唯一标识]` 语法为同一 DOM 元素附加多个 `wp-init` 指令。

`唯一标识` 无需全局唯一，只需在该 DOM 元素的 `wp-init` 指令中保持唯一性即可。

```html
<div data-wp-init="callbacks.logTimeInit">
	<p>您好！</p>
</div>
```

以下示例展示了同一 DOM 元素上多个 `wp-init` 指令的用法：

```html
<form
	data-wp-init--log="callbacks.logTimeInit"
	data-wp-init--focus="callbacks.focusFirstElement"
>
	<input type="text" />
</form>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
import { store, getElement } from '@wordpress/interactivity';

store( "myPlugin", {
  callbacks: {
    logTimeInit: () => console.log( `初始化时间：` + new Date() ),
    focusFirstElement: () => {
      const { ref } = getElement();
      ref.querySelector( 'input:first-child' ).focus(),
    },
  },
} );
```

</details>

若 `wp-init` 指令返回函数，该函数会在元素从 DOM 移除时执行。

### `wp-bind` 指令

该指令允许根据布尔值或字符串值为元素设置 HTML 属性。其语法格式为 `data-wp-bind--attribute`。

```html
<li data-wp-context='{ "isMenuOpen": false }'>
	<button
		data-wp-on--click="actions.toggleMenu"
		data-wp-bind--aria-expanded="context.isMenuOpen"
	>
		切换
	</button>
	<div data-wp-bind--hidden="!context.isMenuOpen">
		<span>标题</span>
		<ul>
			子菜单项
		</ul>
	</div>
</li>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		toggleMenu: () => {
			const context = getContext();
			context.isMenuOpen = ! context.isMenuOpen;
		},
	},
} );
```

</details>

`wp-bind` 指令在以下情况执行：

-   元素创建时
-   每当影响指令最终值的 `state` 或 `context` 属性发生变化时（在回调函数或传递的引用表达式中）

当 `wp-bind` 指令引用回调函数获取最终值时：

-   只要该回调函数内使用的 `state` 或 `context` 属性发生变化，`wp-bind` 指令就会执行
-   回调函数的返回值将用于修改对应属性的值

`wp-bind` 根据值的不同会对 DOM 元素进行不同操作：

-   值为 `true` 时添加属性：`<div attribute>`
-   值为 `false` 时移除属性：`<div>`
-   值为字符串时添加属性并赋值：`<div attribute="value">`
-   属性名以 `aria-` 或 `data-` 开头且值为布尔值时，会将布尔值转为字符串添加到 DOM：`<div aria-attribute="true">`

### `wp-class` 指令

该指令根据布尔值为 HTML 元素添加或移除类名。其语法格式为 `data-wp-class--classname`。

```html
<div>
	<li
		data-wp-context='{ "isSelected": false }'
		data-wp-on--click="actions.toggleSelection"
		data-wp-class--selected="context.isSelected"
	>
		选项 1
	</li>
	<li
		data-wp-context='{ "isSelected": false }'
		data-wp-on--click="actions.toggleSelection"
		data-wp-class--selected="context.isSelected"
	>
		选项 2
	</li>
</div>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		toggleSelection: () => {
			const context = getContext();
			context.isSelected = ! context.isSelected;
		},
	},
} );
```

</details>

`wp-class` 指令在以下情况执行：

-   元素创建时
-   每当影响指令最终值的 `state` 或 `context` 属性发生变化时（在回调函数或传递的引用表达式中）

指令接收的布尔值用于切换 `class` 属性中的关联类名（值为 `true` 时添加，值为 `false` 时移除）。

需要注意：使用 `wp-class` 指令时，推荐使用短横线命名法而非驼峰命名法。因为 HTML 属性不区分大小写，`data-wp-class--isDark`、`data-wp-class--isdark` 和 `DATA-WP-CLASS--ISDARK` 在 HTML 中会被视为相同属性。

因此，建议使用 `is-dark` 替代 `isDark`，使用 `data-wp-class--is-dark` 替代 `data-wp-class--isDark`：

```html
<!-- 推荐用法 -->
<div data-wp-class--is-dark="context.isDarkMode">
	<!-- ... -->
</div>

<!-- 不推荐用法 -->
<div data-wp-class--isDark="context.isDarkMode">
	<!-- ... -->
</div>
```

```css
/* 推荐用法 */
.is-dark {
	/* ... */
}

/* 不推荐用法 */
.isDark {
	/* ... */
}
```

### `wp-style` 指令

该指令根据值为 HTML 元素添加或移除内联样式。其语法格式为 `data-wp-style--css-property`。

```html
<div data-wp-context='{ "color": "red" }'>
	<button data-wp-on--click="actions.toggleContextColor">
		切换文字颜色
	</button>
	<p data-wp-style--color="context.color">Hello World!</p>
</div>
```

<details>
  <summary><em>查看与上述指令配合使用的存储模块</em></summary>

```js
store( 'myPlugin', {
	actions: {
		toggleContextColor: () => {
			const context = getContext();
			context.color = context.color === 'red' ? 'blue' : 'red';
		},
	},
} );
```

</details>

`wp-style` 指令在以下情况执行：

-   元素创建时
-   每当影响指令最终值的 `state` 或 `context` 属性发生变化时（在回调函数或传递的引用表达式中）

指令接收的值用于添加或移除包含对应 CSS 属性的样式属性：

-   值为 `false` 时移除样式属性：`<div>`
-   值为字符串时添加样式属性并赋值：`<div style="css-property: value;">`

### `wp-run` 指令

该指令会在**节点渲染执行期间**运行传入的回调函数。

您可以在传入的回调中使用并组合诸如 `useState`、`useWatch` 或 `useEffect` 之类的钩子，创建自己的逻辑，相比之前的指令提供了更大的灵活性。

您可以通过 `data-wp-run--[唯一标识符]` 语法在同一个 DOM 元素上附加多个 `wp-run` 指令。

`唯一标识符` 不需要全局唯一，只需与该 DOM 元素上其他 `wp-run` 指令的唯一标识符不同即可。

```html
<div data-wp-run="callbacks.logInView">
	<p>你好！</p>
</div>
```

<details>
  <summary><em>查看与上述指令配合使用的存储</em></summary>

```js
import {
	getElement,
	store,
	useState,
	useEffect,
} from '@wordpress/interactivity';

// 与 `data-wp-init` 和 `data-wp-watch` 不同，您可以在 `data-wp-run` 回调中使用任何钩子。
const useInView = () => {
	const [ inView, setInView ] = useState( false );
	useEffect( () => {
		const { ref } = getElement();
		const observer = new IntersectionObserver( ( [ entry ] ) => {
			setInView( entry.isIntersecting );
		} );
		observer.observe( ref );
		return () => ref && observer.unobserve( ref );
	}, [] );
	return inView;
};

store( 'myPlugin', {
	callbacks: {
		logInView: () => {
			const isInView = useInView();
			useEffect( () => {
				if ( isInView ) {
					console.log( '在视口内' );
				} else {
					console.log( '在视口外' );
				}
			} );
		},
	},
} );
```

</details>

需要注意的是，与 (P)React 组件类似，在首次渲染期间，`getElement()` 返回的 `ref` 为 `null`。要正确访问 DOM 元素引用，通常需要使用类似效果的钩子，如 `useEffect`、`useInit` 或 `useWatch`。这确保了 `getElement()` 在组件挂载后运行，并且 `ref` 可用。

### `wp-key` 指令

`wp-key` 指令为元素分配一个唯一键，以帮助交互式 API 在遍历元素数组时识别它。如果您的数组元素可能移动（例如，由于排序）、插入或删除，这一点就变得很重要。选择恰当的键值有助于交互式 API 推断数组中具体发生了哪些变化，从而使其能够正确更新 DOM。

键应是一个字符串，用于在兄弟元素中唯一标识该元素。通常，它用于重复元素，如列表项。例如：

```html
<ul>
	<li data-wp-key="唯一标识符-1">项目 1</li>
	<li data-wp-key="唯一标识符-2">项目 2</li>
</ul>
```

但它也可以用于其他元素：

```html
<div>
	<a data-wp-key="上一页" ...>上一页</a>
	<a data-wp-key="下一页" ...>下一页</a>
</div>
```

当列表重新渲染时，交互式 API 将通过元素的键进行匹配，以确定是否添加/删除/重新排序了项目。没有键的元素可能会不必要地重新创建。

### `wp-each` 指令

`wp-each` 指令用于渲染元素列表。该指令可以在 `<template>` 标签中使用，其值是存储在全局状态或上下文中的数组的路径。`<template>` 标签内的内容是用于渲染每个项目的模板。

默认情况下，每个项目都包含在上下文中的 `item` 名称下，因此模板内的指令可以访问当前项目。

例如，考虑以下 HTML。

```html
<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
	<template data-wp-each="context.list">
		<li data-wp-text="context.item"></li>
	</template>
</ul>
```

它将生成以下输出：

```html
<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
	<li data-wp-text="context.item">hello</li>
	<li data-wp-text="context.item">hola</li>
	<li data-wp-text="context.item">olá</li>
</ul>
```

可以通过向指令名称添加后缀来更改上下文中保存项目的属性。在以下示例中，默认属性从 `item` 更改为 `greeting`。

```html
<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
	<template data-wp-each--greeting="context.list">
		<li data-wp-text="context.greeting"></li>
	</template>
</ul>
```

默认情况下，它使用每个元素作为渲染节点的键，但如果需要，您也可以指定一个路径来检索键，例如当列表包含对象时。

为此，您必须在 `<template>` 标签中使用 `data-wp-each-key`，而不是在模板内容中使用 `data-wp-key`。这是因为 `data-wp-each` 会为每个渲染的项目创建一个上下文提供者包装器，而这些包装器需要 `key` 属性。

```html
<ul
	data-wp-context='{
  "list": [
    { "id": "en", "value": "hello" },
    { "id": "es", "value": "hola" },
    { "id": "pt", "value": "olá" }
  ]
}'
>
	<template
		data-wp-each--greeting="context.list"
		data-wp-each-key="context.greeting.id"
	>
		<li data-wp-text="context.greeting.value"></li>
	</template>
</ul>
```

### `wp-each-child`

针对服务端渲染的列表，另一项名为 `data-wp-each-child` 的指令可确保水合过程按预期工作。该指令在服务器端处理指令时会自动添加。

```html
<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
	<template data-wp-each--greeting="context.list">
		<li data-wp-text="context.greeting"></li>
	</template>
	<li data-wp-each-child>hello</li>
	<li data-wp-each-child>hola</li>
	<li data-wp-each-child>olá</li>
</ul>
```

## 指令的值是对存储属性的引用

分配给指令的值是一个指向特定状态、操作或副作用的字符串。

在以下示例中，使用 getter 定义了派生值 `state.isPlaying`。

```js
const { state } = store( 'myPlugin', {
	state: {
		currentVideo: '',
		get isPlaying() {
			return state.currentVideo !== '';
		},
	},
} );
```

然后，字符串值 `"state.isPlaying"` 用于将此选择器的结果分配给 `data-wp-bind--hidden`。

```html
<div data-wp-bind--hidden="!state.isPlaying" ...>
	<iframe ...></iframe>
</div>
```

分配给指令的这些值是存储中特定属性的**引用**。它们会自动与指令关联，使每个指令“知道”其引用的存储元素，无需任何额外配置。

请注意，默认情况下，引用指向当前命名空间中的属性，该命名空间由具有 `data-wp-interactive` 属性的最近祖先指定。如果需要访问不同命名空间中的属性，可以显式设置属性所在的命名空间。语法为 `namespace::reference`，将 `namespace` 替换为适当的值。

以下示例从 `otherPlugin` 获取 `state.isPlaying`，而不是 `myPlugin`：

```html
<div data-wp-interactive="myPlugin">
	<div data-wp-bind--hidden="otherPlugin::!state.isPlaying" ...>
		<iframe ...></iframe>
	</div>
</div>
```

## 存储

存储用于创建与指令关联的逻辑（操作、副作用等）以及该逻辑中使用的数据（状态、派生状态等）。

**存储通常在每个块的 `view.js` 文件中创建**，但状态可以从块的 `render.php` 文件初始化。

### 存储的组成部分

#### 状态

它定义了页面 HTML 节点可用的数据。重要的是区分两种定义数据的方式：

- **全局状态**：使用带有 `state` 属性的 `store()` 函数定义，数据可供页面的所有 HTML 节点使用。
- **上下文/局部状态**：使用 HTML 节点中的 `data-wp-context` 指令定义，数据可供该 HTML 节点及其子节点使用。可以在操作、派生状态或副作用中使用 `getContext` 函数访问它。

```html
<div data-wp-context='{ "someText": "Hello World!" }'>
	<!-- 访问全局状态 -->
	<span data-wp-text="state.someText"></span>

	<!-- 访问局部状态（上下文） -->
	<span data-wp-text="context.someText"></span>
</div>
```

```js
const { state } = store( 'myPlugin', {
	state: {
		someText: 'Hello Universe!',
	},
	actions: {
		someAction: () => {
			state.someText; // 访问或修改全局状态 - "Hello Universe!"

			const context = getContext();
			context.someText; // 访问或修改局部状态（上下文） - "Hello World!"
		},
	},
} );
```

#### 操作

操作只是普通的 JavaScript 函数。通常由 `data-wp-on` 指令（使用事件监听器）或其他操作触发。

```ts
const { state, actions } = store( 'myPlugin', {
	actions: {
		selectItem: ( id ) => {
			const context = getContext();
			// `id` 在此处是可选的，因此该操作可以在指令中使用。
			state.selected = id || context.id;
		},
		otherAction: () => {
			// 但它也可以从其他操作中调用。
			actions.selectItem( 123 ); // 可以正常工作且类型正确
		},
	},
} );
```

<h5 id="async-actions">异步操作</h5>

异步操作应使用生成器而不是 async/await。

在异步函数中，控制权交给函数本身。函数的调用者无法知道函数是否在等待，更重要的是，等待是否已解决以及函数是否已恢复执行。我们需要这些信息才能恢复作用域。

假设一个块有两个按钮。一个位于 `isOpen: true` 的上下文中，另一个位于 `isOpen: false` 的上下文中：

```html
<div data-wp-context='{ "isOpen": true }'>
	<button data-wp-on--click="actions.someAction">点击</button>
</div>

<div data-wp-context='{ "isOpen": false }'>
	<button data-wp-on--click="actions.someAction">点击</button>
</div>
```

如果操作是异步的并且需要等待较长时间：

- 用户点击第一个按钮。
- 作用域指向第一个上下文，其中 `isOpen: true`。
- 第一次访问 `state.isOpen` 是正确的，因为 `getContext` 返回当前作用域。
- 操作开始等待较长时间。
- 在操作恢复之前，用户点击第二个按钮。
- 作用域切换到第二个上下文，其中 `isOpen: false`。
- 第一次访问 `state.isOpen` 是正确的，因为 `getContext` 返回当前作用域。
- 第二个操作开始等待较长时间。
- 第一个操作完成等待并恢复执行。
- 第一个操作的第二次访问 `state.isOpen` 是错误的，因为 `getContext` 现在返回了错误的作用域。

我们需要能够知道异步操作何时开始等待和恢复操作，以便恢复正确的作用域，这正是生成器的作用。

如果存储按以下方式编写，它将正常工作：

```js
const { state } = store( 'myPlugin', {
	state: {
		get isOpen() {
			return getContext().isOpen;
		},
	},
	actions: {
		someAction: function* () {
			state.isOpen; // 此上下文是正确的，因为它是同步的。
			yield longDelay(); // 使用生成器，调用者控制何时恢复此函数。
			state.isOpen; // 此上下文是正确的，因为我们在恢复函数之前恢复了正确的作用域。
		},
	},
} );
```

如果操作需要处理大量工作，您可能需要在操作中添加多个这样的 `yield` 点。

如上文关于 [`wp-on`](#wp-on)、[`wp-on-window`](#wp-on-window) 和 [`wp-on-document`](#wp-on-document) 所述，当由于操作需要同步访问 `event` 对象而无法使用上述指令的 `async` 版本时，应使用异步操作。当操作需要调用 `event.preventDefault()`、`event.stopPropagation()` 或 `event.stopImmediatePropagation()` 时，需要同步访问。

为确保操作代码不会导致长任务，您可以在调用同步事件 API 后手动让出主线程。Interactivity API 为此提供了 `splitTask()` 函数，它以跨浏览器兼容的方式实现让出。以下是一个示例：

```js
import { splitTask } from '@wordpress/interactivity';

store( 'myPlugin', {
	actions: {
		handleClick: withSyncEvent( function* ( event ) {
			event.preventDefault();
			yield splitTask();
			doTheWork();
		} ),
	},
} );
```

您可能会注意到此示例中使用了 [`withSyncEvent()`](#withsyncevent) 工具函数。这是必要的，因为正在进行的努力是将存储操作默认异步处理，除非它们需要同步事件访问（此示例由于调用了 `event.preventDefault()` 而需要）。否则将触发弃用警告，并在未来的版本中相应更改行为。