gutenbergdocs/docs/reference-guides/interactivity-api/api-reference.md
2025-10-22 01:40:18 +08:00

1410 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 服务器端函数
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()` 而需要)。否则将触发弃用警告,并在未来的版本中相应更改行为。