gutenbergdocs/docs/reference-guides/interactivity-api/api-reference.md

1410 lines
44 KiB
Markdown
Raw Permalink Normal View History

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