docs
This commit is contained in:
124
docs/reference-guides/interactivity-api/README.md
Normal file
124
docs/reference-guides/interactivity-api/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 交互性 API 参考
|
||||
|
||||
交互性 API [随 WordPress 6.5 版本推出](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/),为开发者提供了一种标准化的方式,用于为区块前端添加交互功能。该 API 已被应用于多个 WordPress 核心区块,包括搜索、查询、导航和文件区块。
|
||||
|
||||
这一标准让开发者能够更轻松地创建丰富的交互式用户体验——从简单的计数器或弹窗,到即时页面导航、即时搜索、购物车或结算等复杂功能。
|
||||
|
||||
区块之间可以共享数据、操作和回调函数,这使得区块间的通信更加简便且不易出错。例如:点击“加入购物车”区块时,可以无缝更新独立的“购物车”区块。
|
||||
|
||||
如需了解交互性 API 的诞生背景,请查阅[原始提案](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/)。您也可以查看[合并公告](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/)、[状态更新日志](https://make.wordpress.org/core/2023/08/15/status-update-on-the-interactivity-api/)及官方 [Trac 工单](https://core.trac.wordpress.org/ticket/60356)。
|
||||
|
||||
<div class="callout callout-info">
|
||||
交互性 API 由 <a href="https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/"><code>@wordpress/interactivity</code></a> 软件包提供支持,该包已从 WordPress 6.5 开始内置核心
|
||||
</div>
|
||||
|
||||
## 交互性 API 文档导航
|
||||
|
||||
通过以下链接快速定位您感兴趣的内容。若初次接触交互性 API,建议按顺序阅读以下资源:
|
||||
|
||||
- **[环境要求](#交互性-api-的环境要求)**:开始使用交互性 API 创建交互区块前请查阅本节
|
||||
- **[快速入门指南](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/)**:一分钟内快速创建使用交互性 API 的自定义区块
|
||||
- **[教程:初探交互性 API](https://developer.wordpress.org/news/2024/04/11/a-first-look-at-the-interactivity-api/)**:通过 [WordPress 开发者博客](https://developer.wordpress.org/news/)的这篇文章快速入门
|
||||
- **[核心概念](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/core-concepts/)**:深入理解交互性 API 开发的相关概念与思维模型
|
||||
- **[API 参考](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/)**:深入探索 API 内部机制、指令列表及状态存储工作原理
|
||||
- **[文档与示例](#文档与示例)**:了解更多交互性 API 的延伸资源
|
||||
|
||||
如需深入理解交互性 API 或解答相关疑问,请查阅:
|
||||
|
||||
- **[关于交互性 API](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-about/)**:了解 API 设计目标及采用标准化方案实现区块交互性的核心理念
|
||||
- **[常见问题解答](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-faq/)**:获取技术原理及替代方案的相关解答
|
||||
|
||||
## 交互性 API 的环境要求
|
||||
|
||||
交互性 API 已集成至 WordPress 6.5 核心版本。对于更低版本,需安装并启用 Gutenberg 17.5 或更高版本。
|
||||
|
||||
需要特别说明的是,区块创建工作流程保持不变,所有[环境准备要求](https://developer.wordpress.org/block-editor/getting-started/devenv/)依然适用,包括:
|
||||
|
||||
- [代码编辑器](https://developer.wordpress.org/block-editor/getting-started/devenv/#code-editor)
|
||||
- [Node.js 开发工具](https://developer.wordpress.org/block-editor/getting-started/devenv/#node-js-development-tools)
|
||||
- [本地 WordPress 环境](https://developer.wordpress.org/block-editor/getting-started/devenv/#local-wordpress-environment)
|
||||
|
||||
当您完成区块开发环境配置并运行 WordPress 6.5+(或 Gutenberg 17.5+)后,即可开始创建交互功能。
|
||||
|
||||
### 代码要求
|
||||
|
||||
#### 为项目添加交互功能
|
||||
|
||||
通过以下命令将交互性 API 安装到项目中:
|
||||
|
||||
```bash
|
||||
npm install @wordpress/interactivity --save
|
||||
```
|
||||
|
||||
将存储库导入到 `view.js` 中。更多信息请参阅[存储库文档](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#the-store)。
|
||||
|
||||
```js
|
||||
import { store } from '@wordpress/interactivity';
|
||||
```
|
||||
|
||||
#### 为 `block.json` 添加交互功能支持
|
||||
|
||||
为了表明区块[支持](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/)交互性 API 功能,请在区块的 `block.json` 文件的 `supports` 属性中添加 `"interactivity": true`。
|
||||
|
||||
```json
|
||||
// block.json
|
||||
"supports": {
|
||||
"interactivity": true
|
||||
},
|
||||
```
|
||||
|
||||
有关此属性的详细说明,请参阅[交互性支持属性文档](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#interactivity)。
|
||||
|
||||
#### 使用 `viewScriptModule` 加载交互性 API JavaScript 代码
|
||||
|
||||
交互性 API 提供了 `@wordpress/interactivity` 脚本模块。使用交互性 API 的 JavaScript 应实现为脚本模块,以便它们可以依赖于 `@wordpress/interactivity`。[自 WordPress 6.5 起,脚本模块已可用](https://make.wordpress.org/core/2024/03/04/script-modules-in-6-5/)。区块可以使用 [`viewScriptModule` 区块元数据](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script-module)轻松排队加载其脚本模块:
|
||||
|
||||
```json
|
||||
// block.json
|
||||
{
|
||||
...
|
||||
"viewScriptModule": "file:./view.js"
|
||||
}
|
||||
```
|
||||
|
||||
使用 `viewScriptModule` 还需要在 `wp-scripts` 的 [`build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) 和 [`start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) 脚本中添加 `--experimental-modules` 标志,以确保脚本模块正确构建。
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
...
|
||||
"build": "wp-scripts build --experimental-modules",
|
||||
"start": "wp-scripts start --experimental-modules"
|
||||
}
|
||||
```
|
||||
|
||||
#### 向 DOM 元素添加 `wp-interactive` 指令
|
||||
|
||||
要在 DOM 元素(及其子元素)中“激活”交互性 API,请在区块的 `render.php` 或 `save.js` 文件中向 DOM 元素添加 [`wp-interactive`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/packages-interactivity-api-reference/#wp-interactive) 指令。
|
||||
|
||||
```html
|
||||
<div data-wp-interactive="myPlugin">
|
||||
<!-- 交互性 API 区域 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
有关此指令的详细说明,请参阅 [`wp-interactive` 文档](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#wp-interactive)。
|
||||
|
||||
## 文档与示例
|
||||
|
||||
以下是一些关于交互性 API 的更多学习资源:
|
||||
|
||||
- [WordPress 6.5 开发说明](https://make.wordpress.org/core/2024/03/04/interactivity-api-dev-note/)
|
||||
- [合并公告](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/)
|
||||
- [提案:交互性 API——构建交互式区块的更好开发体验](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/)
|
||||
- [交互性 API 讨论](https://github.com/WordPress/gutenberg/discussions/52882),特别是[展示](https://github.com/WordPress/gutenberg/discussions/55642#discussioncomment-9667164)讨论。
|
||||
- [wpmovies.dev](https://wpmovies.dev/) 演示及其 [wp-movies-demo](https://github.com/WordPress/wp-movies-demo) 仓库
|
||||
- 在 [block-development-examples](https://github.com/WordPress/block-development-examples) 中使用交互性 API 的示例:
|
||||
- [`my-first-interactive-block`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/my-first-interactive-block)
|
||||
- [`interactivity-api-countdown-3cd73e`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-countdown-3cd73e)
|
||||
- [`interactivity-api-quiz-1835fa`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-quiz-1835fa)
|
||||
|
||||
<div class="callout">
|
||||
已开启一个跟踪问题,以便协调与交互性 API 文档相关的工作:<a href="https://github.com/WordPress/gutenberg/issues/53296">交互性 API 文档 - 跟踪问题 #53296</a>
|
||||
</div>
|
||||
1410
docs/reference-guides/interactivity-api/api-reference.md
Normal file
1410
docs/reference-guides/interactivity-api/api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
# 核心概念
|
||||
|
||||
本节提供与交互式API开发相关的重要概念及思维模型的指导说明。通过以下链接深入了解:
|
||||
|
||||
1. **[响应式与声明式思维](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md)**:本指南详解响应式与声明式的核心概念,为高效使用交互式API奠定基础。
|
||||
|
||||
2. **[理解全局状态、局部上下文与派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)**:该指南阐释如何在交互式API中有效运用全局状态、局部上下文及派生状态,强调根据数据作用域和需求选择合适状态管理方案的重要性。
|
||||
|
||||
3. **[服务端渲染:在服务器处理指令](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md)**:交互式API支持WordPress通过服务端渲染生成具有交互性和状态感知的HTML,在保持性能与SEO优势的同时,实现与客户端功能的流畅衔接。
|
||||
|
||||
4. **[使用TypeScript](/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md)**:本指南将带您逐步掌握在交互式API存储库中使用TypeScript的全流程,涵盖从基础类型定义到处理复杂存储结构的高级技巧。
|
||||
@@ -0,0 +1,491 @@
|
||||
## 经典主题中的指令处理
|
||||
|
||||
在交互区块中,只要在 `block.json` 文件中添加 `supports.interactivity`,服务器指令处理便会自动进行。但经典主题呢?
|
||||
|
||||
经典主题同样可以使用交互性 API,如果希望利用服务器指令处理(推荐这样做),可以通过 `wp_interactivity_process_directives` 函数实现。该函数接收包含未处理指令的 HTML 标记,并根据全局状态、局部上下文及派生状态的初始值返回修改后的 HTML 标记。
|
||||
|
||||
```php
|
||||
// 初始化全局状态与派生状态...
|
||||
wp_interactivity_state( '...', /* ... */ );
|
||||
|
||||
// 包含指令的交互式 HTML 标记
|
||||
$html = '<div data-wp-...>...</div>';
|
||||
|
||||
// 处理指令,使其准备好发送至客户端
|
||||
$processed_html = wp_interactivity_process_directives( $html );
|
||||
```
|
||||
|
||||
就这样!无需其他操作。
|
||||
|
||||
若要在模板文件中使用 `wp_interactivity_process_directives`,可通过 `ob_start` 和 `ob_get_clean` 捕获 HTML 输出并在渲染前进行处理。
|
||||
|
||||
```php
|
||||
<?php
|
||||
wp_interactivity_state( 'myClassicTheme', /* ... */ );
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div data-wp-interactive="myClassicTheme">
|
||||
...
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$html = ob_get_clean();
|
||||
echo wp_interactivity_process_directives( $html );
|
||||
```
|
||||
|
||||
**重要提示:** 指令仅需处理一次。如果在某个模板中包含内部模板文件,请确保仅在最外层模板文件中调用 `wp_interactivity_process_directives`,以避免重复处理。
|
||||
|
||||
## 总结
|
||||
|
||||
交互性 API 实现了从服务器端渲染内容到客户端交互的无缝透明过渡。您在服务器端定义的指令、初始全局状态或局部上下文,以及客户端行为,共同构成了统一的生态系统。这种一体化方法简化了开发流程,提升了可维护性,并为创建交互式 WordPress 区块提供了更优的开发体验。
|
||||
|
||||
## 在客户端操作全局状态与局部上下文
|
||||
|
||||
Interactivity API 的核心优势之一在于它弥合了服务端渲染与客户端交互之间的鸿沟。为实现这一点,在服务端初始化的全局状态和局部上下文会被序列化,并供客户端的 Interactivity API 存储库使用,从而使应用能够持续运行并动态操作 DOM。
|
||||
|
||||
让我们扩展这个示例,添加一个用户可点击的按钮,用于向列表中添加新水果:
|
||||
|
||||
```html
|
||||
<button data-wp-on-async--click="actions.addMango">添加芒果</button>
|
||||
```
|
||||
|
||||
这个新按钮具有指向 `actions.addMango` 的 `data-wp-on-async--click` 指令,该操作在 JavaScript 存储库中定义如下:
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
actions: {
|
||||
addMango() {
|
||||
state.fruits.push( '芒果' );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
若使用局部上下文,同样能实现相同效果:
|
||||
|
||||
```javascript
|
||||
store( 'myFruitPlugin', {
|
||||
actions: {
|
||||
addMango() {
|
||||
const context = getContext();
|
||||
context.fruits.push( '芒果' );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
当用户点击“添加芒果”按钮时:
|
||||
|
||||
1. 触发 `addMango` 操作
|
||||
2. 将“芒果”项添加至 `state.fruits`(或 `context.fruits`)数组
|
||||
3. Interactivity API 自动更新 DOM,为新增水果添加 `<li>` 元素
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li>苹果</li>
|
||||
<li>香蕉</li>
|
||||
<li>樱桃</li>
|
||||
<li>芒果</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
重要提示:当状态已在服务端初始化时,客户端无需重复初始化。
|
||||
|
||||
```javascript
|
||||
store( 'myFruitPlugin', {
|
||||
state: {
|
||||
fruits: [ '苹果', '香蕉', '樱桃' ], // 此处无需重复定义!
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
## 在服务端初始化派生状态
|
||||
|
||||
无论派生状态是源自全局状态、局部上下文还是两者结合,均可通过服务端指令处理在服务端进行预处理。
|
||||
|
||||
_建议阅读[理解全局状态、局部上下文与派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)指南,深入了解 Interactivity API 中派生状态的运作机制。_
|
||||
|
||||
### 可静态定义的派生状态
|
||||
|
||||
假设我们需要添加一个删除所有水果的按钮:
|
||||
|
||||
```html
|
||||
<button data-wp-on-async--click="actions.deleteFruits">
|
||||
删除所有水果
|
||||
</button>
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
actions: {
|
||||
// ...
|
||||
deleteFruits() {
|
||||
state.fruits = [];
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
接下来,我们希望在无水果时显示特定提示信息。通过使用引用派生状态 `state.hasFruits` 的 `data-wp-bind--hidden` 指令,可以控制提示信息的显示/隐藏:
|
||||
|
||||
```html
|
||||
<div data-wp-interactive="myFruitPlugin">
|
||||
<ul data-wp-bind--hidden="!state.hasFruits">
|
||||
<template data-wp-each="state.fruits">
|
||||
<li data-wp-text="context.item"></li>
|
||||
</template>
|
||||
</ul>
|
||||
<div data-wp-bind--hidden="state.hasFruits">暂无水果,敬请谅解!</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
派生状态 `state.hasFruits` 在客户端通过 getter 定义:
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
state: {
|
||||
get hasFruits() {
|
||||
return state.fruits.length > 0;
|
||||
},
|
||||
},
|
||||
// ...
|
||||
} );
|
||||
```
|
||||
|
||||
至此,客户端运行正常,点击“删除所有水果”按钮后会显示“暂无水果”提示。但问题在于:由于 `state.hasFruits` 未在服务端定义,`hidden` 属性不会出现在初始 HTML 中,这意味着在 JavaScript 加载完成前,提示信息会持续显示。这不仅会造成访客困惑,还会在 JavaScript 加载完成后因提示信息隐藏而产生布局偏移。
|
||||
|
||||
解决方案是使用 `wp_interactivity_state` 在服务端定义派生状态的初始值:
|
||||
|
||||
- 当初始值已知且为静态值时,可直接定义:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( '苹果', '香蕉', '樱桃' ),
|
||||
'hasFruits' => true
|
||||
));
|
||||
```
|
||||
|
||||
- 或通过必要计算进行定义:
|
||||
|
||||
```php
|
||||
$fruits = array( '苹果', '香蕉', '樱桃' );
|
||||
$hasFruits = count( $fruits ) > 0;
|
||||
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => $fruits,
|
||||
'hasFruits' => $hasFruits,
|
||||
));
|
||||
```
|
||||
|
||||
无论采用哪种方式,核心在于 `state.hasFruits` 的初始值现已在服务端定义。这使得服务端指令处理能够操作 `data-wp-bind--hidden` 指令,并根据需要在 HTML 标记中添加 `hidden` 属性。
|
||||
|
||||
# 服务器端渲染:在服务器上处理指令
|
||||
|
||||
WordPress 始终建立在服务器端渲染的基础之上。传统上,当用户请求一个 WordPress 页面时,服务器会处理 PHP 代码、查询数据库,并生成发送到浏览器的 HTML 标记。
|
||||
|
||||
近年来,像 Vue、React 或 Svelte 这样的现代 JavaScript 框架彻底改变了我们构建 Web 应用程序的方式。这些框架提供了响应式和声明式的编程模型,使开发人员能够轻松创建动态、交互式的用户界面。
|
||||
|
||||
然而,在服务器端渲染方面,这些框架需要一个基于 JavaScript 的服务器(例如 NodeJS)来执行其代码并生成初始 HTML。这意味着像 WordPress 这样基于 PHP 的服务器无法直接利用这些框架,除非牺牲其原生的 PHP 渲染能力。这一限制给希望利用响应式和声明式编程能力,同时又能受益于 WordPress 传统服务器端渲染优势的开发人员带来了挑战。Interactivity API 通过将[响应式和声明式编程原则](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md)引入 WordPress,弥合了这一差距,同时不损害其服务器端渲染的基础。
|
||||
|
||||
在本指南中,我们将探讨 Interactivity API 如何在服务器上处理指令,使 WordPress 能够在初始页面加载时提供交互式、状态感知的 HTML,并为无缝的客户端交互奠定基础。
|
||||
|
||||
## 在服务器上处理指令
|
||||
|
||||
Interactivity API 的服务器指令处理功能使 WordPress 能够生成具有正确交互状态的初始 HTML,从而提供更快的初始渲染。在初始服务器端渲染之后,Interactivity API 的客户端 JavaScript 会接管,实现动态更新和交互,而无需完全重新加载页面。这种方法结合了两者的优点:传统 WordPress 服务器端渲染的 SEO 和性能优势,以及现代 JavaScript 框架提供的动态、响应式用户界面。
|
||||
|
||||
为了理解服务器指令处理的工作原理,让我们从一个使用 `data-wp-each` 指令渲染水果列表的示例开始。
|
||||
|
||||
以下是确保指令在 WordPress 服务器端渲染期间被 Interactivity API 的服务器指令处理正确处理的必要步骤:
|
||||
|
||||
- **1. 将区块标记为交互式**
|
||||
|
||||
首先,要启用交互式区块指令的服务器处理,必须在 `block.json` 中添加 `supports.interactivity`:
|
||||
|
||||
```json
|
||||
{
|
||||
"supports": {
|
||||
"interactivity": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **2. 初始化全局状态或本地上下文**
|
||||
|
||||
然后,必须初始化在页面服务器端渲染期间将使用的全局状态或本地上下文。
|
||||
|
||||
如果使用全局状态,必须使用 `wp_interactivity_state` 函数:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( 'Apple', 'Banana', 'Cherry' )
|
||||
));
|
||||
```
|
||||
|
||||
如果使用本地上下文,初始值通过 `data-wp-context` 指令本身定义,可以通过以下方式之一:
|
||||
|
||||
- 直接添加到 HTML 中。
|
||||
|
||||
```html
|
||||
<ul data-wp-context='{ "fruits": ["Apple", "Banana", "Cherry"] }'>
|
||||
...
|
||||
</ul>
|
||||
```
|
||||
|
||||
- 使用 `wp_interactivity_data_wp_context` 辅助函数。
|
||||
|
||||
```php
|
||||
<?php
|
||||
$context = array( 'fruits' => array( 'Apple', 'Banana', 'Cherry' ) );
|
||||
?>
|
||||
|
||||
<ul <?php echo wp_interactivity_data_wp_context( $context ); ?>>
|
||||
...
|
||||
</ul>
|
||||
```
|
||||
|
||||
- **3. 使用指令定义交互式元素**
|
||||
|
||||
接下来,需要在 HTML 标记中添加必要的指令。
|
||||
|
||||
```html
|
||||
<ul data-wp-interactive="myFruitPlugin">
|
||||
<template data-wp-each="state.fruits">
|
||||
<li data-wp-text="context.item"></li>
|
||||
</template>
|
||||
</ul>
|
||||
```
|
||||
|
||||
在此示例中:
|
||||
|
||||
- `data-wp-interactive` 指令激活 DOM 元素及其子元素的交互性。
|
||||
- `data-wp-each` 指令用于渲染元素列表。该指令可以在 `<template>` 标签中使用,其值是指向存储在全局状态或本地上下文中的数组的引用路径。
|
||||
- `data-wp-text` 指令设置 HTML 元素的内部文本。此处指向 `context.item`,这是 `data-wp-each` 指令存储数组每个项的位置。
|
||||
|
||||
使用本地上下文而非全局状态时,也可以使用完全相同的指令。唯一的区别是 `data-wp-each` 指向 `context.fruits` 而不是 `state.fruits`:
|
||||
|
||||
```html
|
||||
<ul
|
||||
data-wp-interactive="myFruitPlugin"
|
||||
data-wp-context='{ "fruits": [ "Apple", "Banana", "Cherry" ] }'
|
||||
>
|
||||
<template data-wp-each="context.fruits">
|
||||
<li data-wp-text="context.item"></li>
|
||||
</template>
|
||||
</ul>
|
||||
```
|
||||
|
||||
就是这样!一旦你使用 `supports.interactivity` 设置了交互式区块,初始化了全局状态或本地上下文,并将指令添加到 HTML 标记中,Interactivity API 将处理其余的工作。开发人员无需编写额外的代码来在服务器端处理这些指令。
|
||||
|
||||
在幕后,WordPress 使用 `wp_interactivity_process_directives` 函数来查找和处理区块 HTML 标记中的指令。该函数使用 HTML API 根据找到的指令以及初始的全局状态和/或本地上下文对标记进行必要的更改。
|
||||
|
||||
因此,发送到浏览器的 HTML 标记已经是其最终形式,所有指令都已正确处理。这意味着当页面首次在浏览器中加载时,它已经包含了所有交互式元素的正确初始状态,无需任何 JavaScript 来修改它。
|
||||
|
||||
以下是水果列表示例的最终 HTML 标记的样子(指令已省略):
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li>Apple</li>
|
||||
<li>Banana</li>
|
||||
<li>Cherry</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
如你所见,`data-wp-each` 指令为数组中的每个水果生成了一个 `<li>` 元素,并且 `data-wp-text` 指令已被处理,用正确的水果名称填充了每个 `<li>`。
|
||||
|
||||
### 需要动态定义的派生状态
|
||||
|
||||
在大多数情况下,初始派生状态可以像前一个示例那样静态定义。但有时,该值依赖于服务器端也会变化的动态值,此时需要在 PHP 中复现派生逻辑。
|
||||
|
||||
为了演示这种情况,我们继续为每种水果添加购物车表情符号(🛒),根据其是否在购物清单中显示。
|
||||
|
||||
首先,添加一个表示购物清单的数组。*请注意,虽然为了简化示例这些数组是静态的,但通常您会处理动态信息,例如来自数据库的信息。*
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( '苹果', '香蕉', '樱桃' ),
|
||||
'shoppingList' => array( '苹果', '樱桃' ),
|
||||
));
|
||||
```
|
||||
|
||||
现在,在客户端添加一个派生状态,检查每种水果是否在购物清单中并返回对应表情符号。
|
||||
|
||||
```javascript
|
||||
store( 'myFruitPlugin', {
|
||||
state: {
|
||||
get onShoppingList() {
|
||||
const context = getContext();
|
||||
return state.shoppingList.includes( context.item ) ? '🛒' : '';
|
||||
},
|
||||
},
|
||||
// ...
|
||||
} );
|
||||
```
|
||||
|
||||
接着使用该派生状态为每种水果显示对应的表情符号。
|
||||
|
||||
```html
|
||||
<ul data-wp-interactive="myFruitPlugin">
|
||||
<template data-wp-each="state.fruits">
|
||||
<li>
|
||||
<span data-wp-text="context.item"></span>
|
||||
<span data-wp-text="state.onShoppingList"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
```
|
||||
|
||||
至此,客户端一切正常,访问者将看到购物清单中水果旁显示正确的表情符号。但由于 `state.onShoppingList` 未在服务器端定义,表情符号不会出现在初始 HTML 中,需等待 JavaScript 加载后才会显示。
|
||||
|
||||
让我们通过使用 `wp_interactivity_state` 添加初始派生状态来解决这个问题。请注意,这次的值依赖于来自 `data-wp-each` 指令的 `context.item`,这使得派生值具有动态性,因此需要在 PHP 中复现 JavaScript 逻辑:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
// ...
|
||||
'onShoppingList' => function() {
|
||||
$state = wp_interactivity_state();
|
||||
$context = wp_interactivity_get_context();
|
||||
return in_array( $context['item'], $state['shoppingList'] ) ? '🛒' : '';
|
||||
}
|
||||
));
|
||||
```
|
||||
|
||||
完成!现在我们的服务器能够计算派生状态,识别哪些水果在购物清单中。这使得服务器指令处理能够在初始 HTML 中填入正确值,确保用户即使在 JavaScript 运行时加载前也能立即看到正确信息。
|
||||
|
||||
## 序列化其他处理值供客户端使用
|
||||
|
||||
`wp_interactivity_state` 函数对于将服务器处理后的值发送到客户端供后续使用也非常有用。此功能在许多场景中都很实用,例如管理翻译。
|
||||
|
||||
让我们在示例中添加翻译来演示其工作原理。
|
||||
|
||||
```php
|
||||
<?php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( __( '苹果' ), __( '香蕉' ), __( '樱桃' ) ),
|
||||
'shoppingList' => array( __( '苹果' ), __( '樱桃' ) ),
|
||||
// ...
|
||||
?>
|
||||
|
||||
<div data-wp-interactive="myFruitPlugin">
|
||||
<button data-wp-on-async--click="actions.deleteFruits">
|
||||
<?php echo __( '删除所有水果' ); ?>
|
||||
</button>
|
||||
<button data-wp-on-async--click="actions.addMango">
|
||||
<?php echo __( '添加芒果' ); ?>
|
||||
</button>
|
||||
<ul data-wp-bind--hidden="!state.hasFruits">
|
||||
<template data-wp-each="state.fruits">
|
||||
<li>
|
||||
<span data-wp-text="context.item"></span>
|
||||
<span data-wp-text="state.onShoppingList"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<div data-wp-bind--hidden="state.hasFruits">
|
||||
<?php echo __( '暂无水果,抱歉!' ); ?>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
完成!由于交互性 API 在 PHP 中运行,您可以直接将翻译添加到全局状态、本地上下文和 HTML 标记中。
|
||||
|
||||
但请注意我们的 `addMango` 操作!该操作仅在 JavaScript 中定义:
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
actions: {
|
||||
addMango() {
|
||||
state.fruits.push( 'Mango' ); // 未翻译!
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
要解决此问题,可以使用 `wp_interactivity_state` 函数序列化已翻译的芒果字符串,然后在操作中访问该值。
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( __( '苹果' ), __( '香蕉' ), __( '樱桃' ) ),
|
||||
'mango' => __( '芒果' ),
|
||||
));
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
actions: {
|
||||
addMango() {
|
||||
// `state.mango` 包含已翻译的"芒果"字符串
|
||||
state.fruits.push( state.mango );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
如果您的应用更具动态性,可以序列化包含所有水果翻译的数组,并在操作中仅使用*水果关键词*。例如:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myFruitPlugin', array(
|
||||
'fruits' => array( 'apple', 'banana', 'cherry' ),
|
||||
'translatedFruits' => array(
|
||||
'apple' => __( '苹果' ),
|
||||
'banana' => __( '香蕉' ),
|
||||
'cherry' => __( '樱桃' ),
|
||||
'mango' => __( '芒果' ),
|
||||
),
|
||||
'translatedFruit' => function() {
|
||||
$state = wp_interactivity_state();
|
||||
$context = wp_interactivity_get_context();
|
||||
return $state['translatedFruits'][ $context['item'] ];
|
||||
}
|
||||
));
|
||||
```
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myFruitPlugin', {
|
||||
state: {
|
||||
get translatedFruit() {
|
||||
const context = getContext();
|
||||
return state.translatedFruits[ context.item ];
|
||||
}
|
||||
}
|
||||
actions: {
|
||||
addMango() {
|
||||
state.fruits.push( 'mango' );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
```html
|
||||
<template data-wp-each="state.fruits">
|
||||
<li data-wp-text="state.translatedFruit"></li>
|
||||
</template>
|
||||
```
|
||||
|
||||
从服务器序列化信息在其他场景中也很有用,例如传递 Ajax/REST-API URL 和随机数。
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myPlugin', array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'myPlugin_nonce' ),
|
||||
));
|
||||
```
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
actions: {
|
||||
*doSomething() {
|
||||
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 );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
@@ -0,0 +1,318 @@
|
||||
### 可变性与不可变性
|
||||
|
||||
与许多其他响应式框架不同,**交互式 API 在更新全局状态或本地上下文时不需要使用不可变性**。您可以直接修改对象和数组,响应式系统仍将按预期工作。这在许多情况下可以使代码更直观和简洁。
|
||||
|
||||
例如,您可以像这样向数组添加新项:
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myArrayPlugin', {
|
||||
state: {
|
||||
list: [ '项目1', '项目2' ],
|
||||
},
|
||||
actions: {
|
||||
addItem() {
|
||||
// 正确做法:
|
||||
state.list.push( '新项目' );
|
||||
|
||||
// 错误做法:
|
||||
state.list = [ ...state.list, '新项目' ]; // 不要这样做!
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
无需像在其他框架中那样创建新数组或使用展开运算符。交互式 API 将检测此更改并更新依赖于 `state.list` 的任何 UI 部分。
|
||||
|
||||
### 响应式副作用
|
||||
|
||||
除了自动更新 UI 之外,交互式 API 还允许您使用 `data-wp-watch` 等指令在响应式数据更改时执行副作用。副作用对于日志记录、进行 API 调用或更新与 UI 不直接相关的应用程序其他部分等任务非常有用。
|
||||
|
||||
以下是使用 `data-wp-watch` 的示例:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-wp-interactive="myCounterPlugin"
|
||||
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.increment">增加</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
store( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
const context = getContext();
|
||||
context.counter += 1;
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
logCounter: () => {
|
||||
const context = getContext();
|
||||
console.log( `计数器当前值为:${ context.counter }` );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在此示例中:
|
||||
|
||||
1. `data-wp-context` 指令添加了一个本地上下文,其中包含属性 `counter`,其值为 `0`。
|
||||
2. `data-wp-watch` 指令设置为 `callbacks.logCounter`。
|
||||
3. 每次 `context.counter` 更改时,`logCounter` 回调都将执行。
|
||||
4. `logCounter` 回调将当前计数器值记录到控制台。
|
||||
|
||||
这使您可以创建声明式副作用,这些副作用会自动响应数据更改而运行。`data-wp-watch` 的其他一些用例可能包括:
|
||||
|
||||
- 在数据更改时将数据保存到 `localStorage`。
|
||||
- 发送分析事件。
|
||||
- 为无障碍目的更改焦点。
|
||||
- 更新页面标题、元标记或 `<body>` 属性。
|
||||
- 触发动画。
|
||||
|
||||
## 结论
|
||||
|
||||
在使用交互式 API 时,请记住要从状态、操作和副作用的角度思考。定义您的数据,描述应如何更改,然后让交互式 API 处理其余工作。这种思维转变可能需要一些时间,特别是如果您习惯了更命令式的编程风格,但通过接受它,您将释放交互式 API 的全部潜力,以创建真正动态和交互式的 WordPress 块,让您的用户感到愉悦。
|
||||
|
||||
### 你能发现这个错误吗?
|
||||
|
||||
在命令式示例中,为了教学目的故意引入了一个错误。你能找到它吗?这可不容易!
|
||||
|
||||
<details>
|
||||
<summary>查看答案!</summary>
|
||||
|
||||
如果先按下显示按钮,接着按下激活按钮,最后再按下隐藏按钮,代码不会通过`statusParagraph.classList.add('inactive');`添加`inactive`类。因此,当用户下次按下显示按钮时,段落文本将不会显示为红色。
|
||||
|
||||
</details>
|
||||
|
||||
这类错误在命令式代码中非常常见,因为你需要手动控制所有条件。而在声明式代码中则不存在这类问题,因为框架会负责DOM更新,永远不会遗漏任何细节。
|
||||
|
||||
### 声明式方法的优势
|
||||
|
||||
如示例所示,命令式方法需要详细步骤并直接操作DOM,随着交互复杂度的增加,代码会迅速变得复杂且难以维护。可能的状态和元素越多,需要添加的条件逻辑就越多,代码复杂度呈指数级增长。而声明式方法通过状态管理和框架处理DOM更新来简化流程,从而产生更易读、易维护和可扩展的代码。
|
||||
|
||||
## 响应式系统
|
||||
|
||||
得益于对响应式特性的运用,交互式API是一个声明式框架。在响应式系统中,数据的变更会自动触发用户界面的更新,确保视图始终反映应用程序的当前状态。
|
||||
|
||||
### 响应式工作原理
|
||||
|
||||
交互式API采用细粒度响应式系统,其运作方式如下:
|
||||
|
||||
1. **响应式状态**:在交互式API中,全局状态和本地上下文都是响应式的。这意味着当这些数据源发生变化时,依赖它们的任何UI部分都会自动更新。
|
||||
|
||||
- **全局状态**:这是可在整个交互块中访问的全局数据
|
||||
- **本地上下文**:这是特定元素及其子元素专属的本地数据
|
||||
- **派生状态**:除了基础状态属性外,您还可以定义计算属性,这些属性会在其依赖项变更时自动更新
|
||||
|
||||
_请访问[理解全局状态、本地上下文和派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)指南,详细了解如何在交互式API中使用不同类型的响应式状态。_
|
||||
|
||||
2. **操作**:这些通常由事件处理程序触发的函数,用于变更全局状态或本地上下文
|
||||
|
||||
3. **响应式绑定**:使用特殊属性(如`data-wp-bind`、`data-wp-text`或`data-wp-class`)将HTML元素与响应式状态值绑定
|
||||
|
||||
4. **自动更新**:当操作变更全局状态或本地上下文时,交互式API会自动更新依赖该状态的所有DOM部分(直接依赖或通过派生状态间接依赖)
|
||||
|
||||
让我们通过分析之前的示例来解析这些概念:
|
||||
|
||||
```javascript
|
||||
const { state } = store( 'myInteractivePlugin', {
|
||||
state: {
|
||||
isVisible: false,
|
||||
isActive: false,
|
||||
get visibilityText() {
|
||||
return state.isVisible ? 'hide' : 'show';
|
||||
},
|
||||
// ... 其他派生状态
|
||||
},
|
||||
actions: {
|
||||
toggleVisibility() {
|
||||
state.isVisible = ! state.isVisible;
|
||||
},
|
||||
// ... 其他操作
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在这段代码中:
|
||||
|
||||
- `isVisible`和`isActive`是基础状态属性
|
||||
- `visibilityText`是派生状态,会在`isVisible`变更时自动更新
|
||||
- `toggleVisibility`是修改状态的操作
|
||||
|
||||
HTML绑定如下所示:
|
||||
|
||||
```html
|
||||
<button
|
||||
data-wp-on--click="actions.toggleVisibility"
|
||||
data-wp-text="state.visibilityText"
|
||||
data-wp-bind--aria-expanded="state.isVisible"
|
||||
>
|
||||
show
|
||||
</button>
|
||||
```
|
||||
|
||||
响应式机制的实际运作流程:
|
||||
|
||||
1. 当按钮被点击时,触发`toggleVisibility`操作
|
||||
2. 该操作更新`state.isVisible`
|
||||
3. 交互式API检测到此变更并自动:
|
||||
- 更新按钮的文本内容(因为`data-wp-text="state.visibilityText"`)
|
||||
- 更改`aria-expanded`属性(由于`data-wp-bind--aria-expanded="state.isVisible"`)
|
||||
- 更新任何其他依赖`isVisible`或`visibilityText`的DOM部分
|
||||
|
||||
# 响应式与声明式思维模式
|
||||
|
||||
交互性API是一个响应式声明式框架,与其他现代框架(如React、Vue、Svelte或Alpine)类似。在使用交互性API时,采用正确的思维模式对于充分发挥其潜力至关重要。本指南将解释响应式和声明式的核心概念,为有效使用交互性API奠定基础。
|
||||
|
||||
## 声明式 vs 命令式
|
||||
|
||||
**声明式编程**描述的是程序_应该实现什么_,它关注期望的结果,而不显式列出实现该结果的命令或步骤。相比之下,**命令式编程**通过明确说明操作程序状态的每个步骤来指定_如何_完成任务。
|
||||
|
||||
### 命令式方法
|
||||
|
||||
在Web开发的早期阶段,命令式方法占据主导地位。这种方法涉及使用JavaScript手动更新DOM以反映变化。
|
||||
|
||||
以这个包含两个按钮和一个段落的交互式区块为例:
|
||||
|
||||
- **显示/隐藏按钮**:切换段落可见性并启用/禁用"激活"按钮
|
||||
- **激活/停用按钮**:在"激活"(绿色)和"非激活"(红色)状态间切换段落文本和颜色
|
||||
|
||||
```html
|
||||
<div id="my-interactive-plugin">
|
||||
<button
|
||||
id="show-hide-btn"
|
||||
aria-expanded="false"
|
||||
aria-controls="status-paragraph"
|
||||
>
|
||||
显示
|
||||
</button>
|
||||
<button id="activate-btn" disabled>激活</button>
|
||||
<p id="status-paragraph" class="inactive" hidden>这是非激活状态</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.active {
|
||||
color: green;
|
||||
}
|
||||
.inactive {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const showHideBtn = document.getElementById( 'show-hide-btn' );
|
||||
const activateBtn = document.getElementById( 'activate-btn' );
|
||||
const statusParagraph = document.getElementById( 'status-paragraph' );
|
||||
|
||||
showHideBtn.addEventListener( 'click', () => {
|
||||
if ( statusParagraph.hasAttribute( 'hidden' ) ) {
|
||||
statusParagraph.removeAttribute( 'hidden' );
|
||||
showHideBtn.textContent = '隐藏';
|
||||
showHideBtn.setAttribute( 'aria-expanded', 'true' );
|
||||
activateBtn.removeAttribute( 'disabled' );
|
||||
} else {
|
||||
if ( statusParagraph.classList.contains( 'active' ) ) {
|
||||
statusParagraph.textContent = '这是非激活状态';
|
||||
statusParagraph.classList.remove( 'active' );
|
||||
activateBtn.textContent = '激活';
|
||||
}
|
||||
statusParagraph.setAttribute( 'hidden', true );
|
||||
showHideBtn.textContent = '显示';
|
||||
showHideBtn.setAttribute( 'aria-expanded', 'false' );
|
||||
activateBtn.setAttribute( 'disabled', true );
|
||||
}
|
||||
} );
|
||||
|
||||
activateBtn.addEventListener( 'click', () => {
|
||||
if ( activateBtn.textContent === '激活' ) {
|
||||
statusParagraph.textContent = '这是激活状态';
|
||||
statusParagraph.classList.remove( 'inactive' );
|
||||
statusParagraph.classList.add( 'active' );
|
||||
activateBtn.textContent = '停用';
|
||||
} else {
|
||||
statusParagraph.textContent = '这是非激活状态';
|
||||
statusParagraph.classList.remove( 'active' );
|
||||
statusParagraph.classList.add( 'inactive' );
|
||||
activateBtn.textContent = '激活';
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
```
|
||||
|
||||
如您所见,对于每种情况,您都必须使用JavaScript来修改DOM中所有已更改的内容,同时还需要考虑之前的状态。
|
||||
|
||||
### 声明式方法
|
||||
|
||||
声明式方法通过关注_应该发生什么_来简化流程。用户界面会根据状态变化自动更新。以下是使用交互性API声明式方法的类似示例:
|
||||
|
||||
```html
|
||||
<div id="my-interactive-plugin" data-wp-interactive="myInteractivePlugin">
|
||||
<button
|
||||
data-wp-on--click="actions.toggleVisibility"
|
||||
data-wp-bind--aria-expanded="state.isVisible"
|
||||
data-wp-text="state.visibilityText"
|
||||
aria-controls="status-paragraph"
|
||||
>
|
||||
显示
|
||||
</button>
|
||||
<button
|
||||
data-wp-on--click="actions.toggleActivation"
|
||||
data-wp-bind--disabled="!state.isVisible"
|
||||
data-wp-text="state.activationText"
|
||||
>
|
||||
激活
|
||||
</button>
|
||||
<p
|
||||
id="status-paragraph"
|
||||
data-wp-bind--hidden="!state.isVisible"
|
||||
data-wp-class--active="state.isActive"
|
||||
data-wp-class--inactive="!state.isActive"
|
||||
data-wp-text="state.paragraphText"
|
||||
>
|
||||
这是非激活状态
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.active {
|
||||
color: green;
|
||||
}
|
||||
.inactive {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```js
|
||||
import { store } from '@wordpress/interactivity';
|
||||
|
||||
const { state } = store( 'myInteractivePlugin', {
|
||||
state: {
|
||||
isVisible: false,
|
||||
isActive: false,
|
||||
get visibilityText() {
|
||||
return state.isVisible ? '隐藏' : '显示';
|
||||
},
|
||||
get activationText() {
|
||||
return state.isActive ? '停用' : '激活';
|
||||
},
|
||||
get paragraphText() {
|
||||
return state.isActive ? '这是激活状态' : '这是非激活状态';
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
toggleVisibility() {
|
||||
state.isVisible = ! state.isVisible;
|
||||
if ( ! state.isVisible ) state.isActive = false;
|
||||
},
|
||||
toggleActivation() {
|
||||
state.isActive = ! state.isActive;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在这个声明式示例中,用户界面会根据当前状态自动更新。作为开发人员,您只需要声明必要的状态、任何派生状态、修改状态的操作,以及DOM的哪些部分依赖于状态的哪些部分。框架会负责对DOM进行所有必要的更新,使其始终与当前状态保持同步。无论框架控制的元素数量有多少,逻辑都保持简单且易于维护。
|
||||
@@ -0,0 +1,839 @@
|
||||
### 使用时机
|
||||
|
||||
当存在依赖全局状态或本地上下文的交互模块,且这些内容可能因导航事件而改变时,使用此功能可确保应用程序不同部分的一致性。
|
||||
|
||||
### `getServerState()` 与 `getServerContext()` 使用最佳实践
|
||||
|
||||
- **只读引用:** `getServerState()` 和 `getServerContext()` 均返回只读对象。您可以使用这些对象来更新全局状态或本地上下文。
|
||||
- **回调集成:** 在存储[回调函数](/docs/reference-guides/interactivity-api/api-reference.md#accessing-data-in-callbacks)中集成这些函数,以响应状态和上下文的变化。`getServerState()` 和 `getServerContext()` 返回的都是响应式对象,这意味着它们的监听回调仅在属性值发生变化时触发。若属性值保持不变,则不会重新触发回调。
|
||||
|
||||
## 总结
|
||||
|
||||
请记住,有效状态管理的关键在于保持状态最小化并避免冗余。使用派生状态动态计算值,根据数据的作用域和需求在全局状态与本地上下文之间做出选择。这将有助于构建更清晰、更健壮的架构,使调试和维护更为轻松。最后,若需将状态或上下文与服务器同步,可通过 `getServerState()` 和 `getServerContext()` 实现这一目标。
|
||||
|
||||
### 示例:使用局部上下文实现独立状态的交互区块
|
||||
|
||||
此示例展示了一个交互区块,它显示计数器并支持递增操作。通过使用局部上下文,该区块的每个实例都将拥有独立的计数器,即使页面中添加了多个此类区块。
|
||||
|
||||
```php
|
||||
<div
|
||||
data-wp-interactive="myCounterPlugin"
|
||||
<?php echo get_block_wrapper_attributes(); ?>
|
||||
data-wp-context='{ "counter": 0 }'
|
||||
>
|
||||
<p>计数器:<span data-wp-text="context.counter"></span></p>
|
||||
<button data-wp-on-async--click="actions.increment">递增</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
store( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
const context = getContext();
|
||||
context.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
本示例说明:
|
||||
|
||||
1. 通过 `data-wp-context` 指令定义初始值为 `0` 的局部计数器上下文
|
||||
2. 使用 `data-wp-text="context.counter"` 从局部上下文读取并显示计数器值
|
||||
3. 递增按钮通过 `data-wp-on-async--click="actions.increment"` 触发递增操作
|
||||
4. JavaScript 中的 `getContext` 函数用于访问和修改每个区块实例的局部上下文
|
||||
|
||||
用户可在页面中添加多个该区块实例,每个实例都将保持独立的计数器状态。点击某个区块的"递增"按钮仅影响该特定区块的计数器,不会对其他区块产生作用。
|
||||
|
||||
## 派生状态
|
||||
|
||||
Interactivity API 中的**派生状态**指根据全局状态或局部上下文的其他部分计算得出的值。该值按需计算而非直接存储,可确保一致性、减少冗余,并增强代码的声明性特性。
|
||||
|
||||
派生状态是现代状态管理的基础概念,并非 Interactivity API 独有。在其他主流状态管理系统中同样存在,例如 Redux 中称为“选择器”(selectors),Preact Signals 中称作“计算值”(computed values)。
|
||||
|
||||
派生状态具有以下核心优势,使其成为精心设计的应用状态中不可或缺的组成部分:
|
||||
|
||||
1. **单一数据源**:派生状态鼓励仅存储必要的原始数据,所有可基于核心数据计算的值都作为派生状态。这种方法可降低交互区块中出现数据不一致的风险。
|
||||
|
||||
2. **自动更新**:使用派生状态时,当基础数据发生变化,相关值会自动重新计算。这能确保交互区块的所有部分始终获取最新信息,无需人工干预。
|
||||
|
||||
3. **简化状态管理**:通过按需计算而非手动存储更新数值,可降低状态管理逻辑的复杂度,使代码更清晰、更易维护。
|
||||
|
||||
4. **提升性能**:多数情况下,派生状态可优化为仅在必要时重新计算,从而提升交互区块的性能表现。
|
||||
|
||||
5. **便于调试**:派生状态能更清晰地展现数据来源与转换过程,有助于快速定位交互区块中的问题。
|
||||
|
||||
本质上,派生状态允许以声明式方式表达交互区块中不同数据间的关联,而非在数据变化时强制更新相关值。
|
||||
|
||||
_请访问[响应式与声明式思维指南](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md),深入了解如何在 Interactivity API 中运用声明式编码。_
|
||||
|
||||
在以下场景中建议使用派生状态:
|
||||
|
||||
- 当部分全局状态或局部上下文可从其他状态值计算得出时
|
||||
- 需要避免手动保持同步的冗余数据时
|
||||
- 通过自动更新派生值确保交互区块间的一致性时
|
||||
- 通过消除更新多个关联状态属性的需求来简化操作时
|
||||
|
||||
## 订阅服务端状态与上下文
|
||||
|
||||
交互性API提供基于区域的路由功能,能够动态替换页面局部内容而无需整页刷新。当禁用"强制页面重载"开关时,[查询区块](/docs/reference-guides/core-blocks.md#query-loop)原生支持此功能。开发者可通过调用 [`@wordpress/interactivity-router`](https://github.com/WordPress/gutenberg/tree/trunk/packages/interactivity-router) 脚本模块中的 [`actions.navigate()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity-router/#actions) 在自定义区块中实现相同功能。
|
||||
|
||||
使用基于区域的路由时,必须确保交互区块与服务端提供的全局状态和本地上下文保持同步。默认情况下,交互性API不会用服务端传值覆盖全局状态或本地上下文。该API提供两个函数来协助管理同步:[`getServerState()`](/docs/reference-guides/interactivity-api/api-reference.md#getserverstate) 和 [`getServerContext()`](/docs/reference-guides/interactivity-api/api-reference.md#getservercontext)。
|
||||
|
||||
### `getServerState()`
|
||||
|
||||
`getServerState()` 用于订阅**全局状态**在客户端导航期间发生的变化。此函数与 `getServerContext()` 类似,但作用于全局状态而非本地上下文。
|
||||
|
||||
`getServerState()` 返回一个只读的响应式对象。这意味着任何监视该返回对象的[回调函数](/docs/reference-guides/interactivity-api/api-reference.md#accessing-data-in-callbacks)仅会在函数返回值变更时触发。若值保持不变,回调不会重复触发。
|
||||
|
||||
以多题目测验为例:每个题目位于独立页面,当用户导航至新题目时,服务端将提供新题目及剩余答题时间。
|
||||
|
||||
```php
|
||||
<div <?php echo wp_interactivity_state( 'myPlugin', array(
|
||||
'question' => get_question_for_page( get_the_ID() ),
|
||||
'timeLeft' => 5 * 60, // 回答所有题目的总剩余时间
|
||||
) ); ?>>
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { store, getServerState } from '@wordpress/interactivity';
|
||||
|
||||
store( 'myPlugin', {
|
||||
actions: {
|
||||
// 此操作通过指令触发,例如:
|
||||
// <button data-wp-on-click="actions.nextQuestion">下一题</button>
|
||||
*nextQuestion() {
|
||||
event.preventDefault( event );
|
||||
const { actions } = yield import(
|
||||
'@wordpress/interactivity-router'
|
||||
);
|
||||
actions.navigate( '/question-2' );
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
// 此回调通过指令触发,例如:
|
||||
// <div data-wp-watch="callbacks.updateQuestion"></div>
|
||||
updateQuestion() {
|
||||
const serverState = getServerState();
|
||||
|
||||
// 使用服务端的新值更新
|
||||
// 注意不更新`timeLeft`,因其表示所有题目的总剩余时间
|
||||
state.question = serverState.question;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
### `getServerContext()`
|
||||
|
||||
`getServerContext()` 用于订阅**本地上下文**在客户端导航期间发生的变化。此函数与 `getServerState()` 类似,但作用于本地上下文而非全局状态。
|
||||
|
||||
`getServerContext()` 返回一个只读的响应式对象。这意味着任何监视该返回对象的[回调函数](/docs/reference-guides/interactivity-api/api-reference.md#accessing-data-in-callbacks)仅会在函数返回值变更时触发。若值保持不变,回调不会重复触发。
|
||||
|
||||
以多题目测验为例:每个题目位于独立页面,当用户导航至新题目时,服务端将提供新题目及剩余答题时间。
|
||||
|
||||
```php
|
||||
<div <?php echo wp_interactivity_data_wp_context( array(
|
||||
'currentQuestion' => get_question_for_page( get_the_ID() ),
|
||||
), ); ?>>
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { store, getServerContext } from '@wordpress/interactivity';
|
||||
|
||||
store( 'myPlugin', {
|
||||
actions: {
|
||||
// 此操作通过指令触发,例如:
|
||||
// <button data-wp-on-click="actions.nextQuestion">下一题</button>
|
||||
*nextQuestion() {
|
||||
event.preventDefault( event );
|
||||
const { actions } = yield import(
|
||||
'@wordpress/interactivity-router'
|
||||
);
|
||||
actions.navigate( '/question-2' );
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
// 此回调通过指令触发,例如:
|
||||
// <div data-wp-watch="callbacks.updateQuestion"></div>
|
||||
updateQuestion() {
|
||||
const serverContext = getServerContext();
|
||||
const context = getContext();
|
||||
|
||||
// 使用服务端的新值更新
|
||||
context.currentQuestion = serverContext.currentQuestion;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
### 示例:未使用派生状态 vs 使用派生状态
|
||||
|
||||
考虑一个需要显示计数器及其两倍数值的场景,我们对比两种实现方式:未使用派生状态与使用派生状态。
|
||||
|
||||
- **未使用派生状态**
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 1,
|
||||
double: 2,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
state.double = state.counter * 2;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
这种方式需要在 `increment` 操作中手动更新 `state.counter` 和 `state.double` 的值。虽然可行,但存在以下缺陷:
|
||||
|
||||
- 声明性较弱
|
||||
- 当从多个位置更新 `state.counter` 时,若开发者忘记同步更新 `state.double` 可能导致错误
|
||||
- 需要额外关注相关值的更新,增加认知负担
|
||||
|
||||
- **使用派生状态**
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 1,
|
||||
get double() {
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
这个改进版本具有以下优势:
|
||||
|
||||
- `state.double` 被定义为获取器,自动从 `state.counter` 派生值
|
||||
- `increment` 操作只需更新 `state.counter`
|
||||
- 无论 `state.counter` 在何时何地更新,`state.double` 总能保持正确值
|
||||
|
||||
### 示例:在本地上下文中使用派生状态
|
||||
|
||||
考虑一个初始化计数器的本地上下文场景:
|
||||
|
||||
```js
|
||||
store( 'myCounterPlugin', {
|
||||
state: {
|
||||
get double() {
|
||||
const { counter } = getContext();
|
||||
return counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
const context = getContext();
|
||||
context.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
```html
|
||||
<div data-wp-interactive="myCounterPlugin">
|
||||
<!-- 这里将显示 "Double: 2" -->
|
||||
<div data-wp-context='{ "counter": 1 }'>
|
||||
两倍值:<span data-wp-text="state.double"></span>
|
||||
|
||||
<!-- 此按钮将增加本地计数器 -->
|
||||
<button data-wp-on-async--click="actions.increment">递增</button>
|
||||
</div>
|
||||
|
||||
<!-- 这里将显示 "Double: 4" -->
|
||||
<div data-wp-context='{ "counter": 2 }'>
|
||||
两倍值:<span data-wp-text="state.double"></span>
|
||||
|
||||
<!-- 此按钮将增加本地计数器 -->
|
||||
<button data-wp-on-async--click="actions.increment">递增</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
在此示例中,派生状态 `state.double` 从每个元素所在的本地上下文读取数据,为每个使用实例返回正确的值。
|
||||
|
||||
### 示例:同时使用本地上下文和全局状态的派生状态
|
||||
|
||||
考虑一个包含全局税率和本地商品价格,并计算含税最终价格的场景:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-wp-interactive="myProductPlugin"
|
||||
data-wp-context='{ "priceWithoutTax": 100 }'
|
||||
>
|
||||
<p>商品价格:$<span data-wp-text="context.priceWithoutTax"></span></p>
|
||||
<p>税率:<span data-wp-text="state.taxRatePercentage"></span></p>
|
||||
<p>含税价格:$<span data-wp-text="state.priceWithTax"></span></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
const { state } = store( 'myProductPlugin', {
|
||||
state: {
|
||||
taxRate: 0.21,
|
||||
get taxRatePercentage() {
|
||||
return `${ state.taxRate * 100 }%`;
|
||||
},
|
||||
get priceWithTax() {
|
||||
const { priceWithoutTax } = getContext();
|
||||
return priceWithoutTax * ( 1 + state.taxRate );
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
updateTaxRate( event ) {
|
||||
// 更新全局税率
|
||||
state.taxRate = event.target.value;
|
||||
},
|
||||
updatePrice( event ) {
|
||||
// 更新本地商品价格
|
||||
const context = getContext();
|
||||
context.priceWithoutTax = event.target.value;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在此示例中,`priceWithTax` 同时从全局 `taxRate` 和本地 `priceWithoutTax` 派生。当通过 `updateTaxRate` 或 `updatePrice` 操作更新全局状态或本地上下文时,交互性 API 会重新计算派生状态并更新 DOM 中必要的部分。
|
||||
|
||||
通过使用派生状态,您可以创建更易维护、更不易出错的代码库。这能确保相关状态值始终保持同步,降低操作逻辑的复杂度,使代码更具声明性且更易于理解。
|
||||
|
||||
### 使用派生状态
|
||||
|
||||
- **初始化派生状态**
|
||||
|
||||
通常,派生状态应使用 `wp_interactivity_state` 函数在服务器上进行初始化,其方式与全局状态完全相同。
|
||||
|
||||
- 当初始值已知且为静态时,可以直接定义:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'counter' => 1, // 这是全局状态。
|
||||
'double' => 2, // 这是派生状态。
|
||||
));
|
||||
```
|
||||
|
||||
- 或者可以通过进行必要的计算来定义:
|
||||
|
||||
```php
|
||||
$counter = 1;
|
||||
$double = $counter * 2;
|
||||
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'counter' => $counter, // 这是全局状态。
|
||||
'double' => $double, // 这是派生状态。
|
||||
));
|
||||
```
|
||||
|
||||
无论采用哪种方法,初始的派生状态值将在 PHP 渲染页面时使用,并且 HTML 将填充正确的值。
|
||||
|
||||
_请访问 [服务端渲染指南](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md) 以了解更多关于指令在服务器上如何处理的信息。_
|
||||
|
||||
即使派生状态属性依赖于本地上下文,同样的机制也适用。
|
||||
|
||||
```php
|
||||
<?php
|
||||
$counter = 1;
|
||||
|
||||
// 这是本地上下文。
|
||||
$context = array( 'counter' => $counter );
|
||||
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'double' => $counter * 2, // 这是派生状态。
|
||||
));
|
||||
?>
|
||||
|
||||
<div
|
||||
data-wp-interactive="myCounterPlugin"
|
||||
<?php echo wp_interactivity_data_wp_context( $context ); ?>
|
||||
>
|
||||
<div>
|
||||
计数器:<span data-wp-text="context.counter"></span>
|
||||
</div>
|
||||
<div>
|
||||
双倍:<span data-wp-text="state.double"></span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
在 JavaScript 中,派生状态使用 getter 定义:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
get double() {
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
派生状态可以依赖于本地上下文,或者同时依赖于本地上下文和全局状态。
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
get double() {
|
||||
const { counter } = getContext();
|
||||
// 依赖于本地上下文。
|
||||
return counter * 2;
|
||||
},
|
||||
get product() {
|
||||
const { counter } = getContext();
|
||||
// 依赖于本地上下文和全局状态。
|
||||
return counter * state.factor;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在某些情况下,当派生状态依赖于本地上下文,并且本地上下文在服务器上可以动态更改时,可以使用函数(闭包)动态计算派生状态,而不是使用初始派生状态。
|
||||
|
||||
```php
|
||||
<?php
|
||||
wp_interactivity_state( 'myProductPlugin', array(
|
||||
'list' => array( 1, 2, 3 ),
|
||||
'factor' => 3,
|
||||
'product' => function() {
|
||||
$state = wp_interactivity_state();
|
||||
$context = wp_interactivity_get_context();
|
||||
return $context['item'] * $state['factor'];
|
||||
}
|
||||
));
|
||||
?>
|
||||
|
||||
<template
|
||||
data-wp-interactive="myProductPlugin"
|
||||
data-wp-each="state.list"
|
||||
>
|
||||
<span data-wp-text="state.product"></span>
|
||||
</template>
|
||||
```
|
||||
|
||||
此 `data-wp-each` 模板将渲染以下 HTML(指令省略):
|
||||
|
||||
```html
|
||||
<span>3</span>
|
||||
<span>6</span>
|
||||
<span>9</span>
|
||||
```
|
||||
|
||||
- **访问派生状态**
|
||||
|
||||
在 HTML 标记中,派生状态的语法与全局状态的语法相同,只需在指令属性值中引用 `state`。
|
||||
|
||||
```html
|
||||
<span data-wp-text="state.double"></span>
|
||||
```
|
||||
|
||||
在 JavaScript 中也是如此。全局状态和派生状态都可以通过 store 的 `state` 属性使用:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
// ...
|
||||
actions: {
|
||||
readValues() {
|
||||
state.counter; // 常规状态,返回 1。
|
||||
state.double; // 派生状态,返回 2。
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
这种无区别的设计是有意的,允许开发者统一使用派生状态和全局状态,并使它们在实践中可以互换。
|
||||
|
||||
你还可以从另一个派生状态访问派生状态,从而创建多级计算值。
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
state: {
|
||||
get double() {
|
||||
return state.counter * 2;
|
||||
},
|
||||
get doublePlusOne() {
|
||||
return state.double + 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
- **更新派生状态**
|
||||
|
||||
派生状态不能直接更新。要更新其值,你需要更新该派生状态所依赖的全局状态或本地上下文。
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
// ...
|
||||
actions: {
|
||||
updateValues() {
|
||||
state.counter; // 常规状态,返回 1。
|
||||
state.double; // 派生状态,返回 2。
|
||||
|
||||
state.counter = 2;
|
||||
|
||||
state.counter; // 常规状态,返回 2。
|
||||
state.double; // 派生状态,返回 4。
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
### 示例:使用全局状态通信的两个交互块
|
||||
|
||||
本示例包含两个独立的交互块:一个用于显示计数器,另一个包含递增计数器的按钮。这些块可以放置在页面任意位置,无需遵循特定的HTML结构。也就是说,一个块不必作为另一个块的内嵌块存在。
|
||||
|
||||
- **计数器块**
|
||||
|
||||
```php
|
||||
<?php
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'counter' => 0
|
||||
));
|
||||
?>
|
||||
|
||||
<div
|
||||
data-wp-interactive="myCounterPlugin"
|
||||
<?php echo get_block_wrapper_attributes(); ?>
|
||||
>
|
||||
计数器:<span data-wp-text="state.counter"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
- **递增块**
|
||||
|
||||
```php
|
||||
<div
|
||||
data-wp-interactive="myCounterPlugin"
|
||||
<?php echo get_block_wrapper_attributes(); ?>
|
||||
>
|
||||
<button data-wp-on-async--click="actions.increment">
|
||||
递增
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
本示例说明:
|
||||
|
||||
1. 通过服务端的 `wp_interactivity_state` 初始化全局状态,设置计数器初始值为0
|
||||
2. 计数器块使用 `data-wp-text="state.counter"` 读取全局状态并显示当前计数值
|
||||
3. 递增块包含的按钮通过 `data-wp-on-async--click="actions.increment"` 在点击时触发递增操作
|
||||
4. JavaScript中的 `increment` 操作通过递增 `state.counter` 直接修改全局状态
|
||||
|
||||
这两个块相互独立,可置于页面任意位置,无需在DOM结构中形成嵌套或直接关联。页面中可以添加多个此类交互块实例,它们将共享并更新同一全局计数值。
|
||||
|
||||
## 本地上下文
|
||||
|
||||
Interactivity API中的**本地上下文**特指在HTML结构特定元素内定义的局部数据。与全局状态不同,本地上下文仅可在定义元素及其子元素中访问。
|
||||
|
||||
本地上下文在以下场景中特别有用:
|
||||
- 需要为多个交互块实例维护独立状态
|
||||
- 需要封装仅与特定交互块及其子元素相关的数据
|
||||
- 需要实现仅限于界面特定区域的隔离功能
|
||||
|
||||
### 本地上下文操作指南
|
||||
|
||||
- **初始化本地上下文**
|
||||
|
||||
通过 `data-wp-context` 指令直接在HTML结构中初始化本地上下文,该指令接收定义上下文初始值的JSON字符串:
|
||||
|
||||
```html
|
||||
<div data-wp-context='{ "counter": 0 }'>
|
||||
<!-- 子元素可访问 `context.counter` -->
|
||||
</div>
|
||||
```
|
||||
|
||||
也可使用 `wp_interactivity_data_wp_context` PHP辅助函数在服务端初始化,确保字符串化值的正确转义和格式化:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$context = array( 'counter' => 0 );
|
||||
?>
|
||||
|
||||
<div <?php echo wp_interactivity_data_wp_context( $context ); ?>>
|
||||
<!-- 子元素可访问 `context.counter` -->
|
||||
</div>
|
||||
```
|
||||
|
||||
- **访问本地上下文**
|
||||
|
||||
在HTML标记中,可通过指令值直接引用 `context` 访问本地上下文值:
|
||||
|
||||
```html
|
||||
<div data-wp-bind--hidden="!context.isOpen">
|
||||
<span data-wp-text="context.counter"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
在JavaScript中,可使用 `getContext` 函数访问本地上下文值:
|
||||
|
||||
```js
|
||||
store( 'myPlugin', {
|
||||
actions: {
|
||||
sendAnalyticsEvent() {
|
||||
const { counter } = getContext();
|
||||
myAnalyticsLibrary.sendEvent( 'updated counter', counter );
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
logCounter() {
|
||||
const { counter } = getContext();
|
||||
console.log( `当前计数值:${ counter }` );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
`getContext` 函数返回触发操作/回调执行的元素对应的本地上下文。
|
||||
|
||||
- **更新本地上下文**
|
||||
|
||||
在JavaScript中可通过修改 `getContext` 返回的对象来更新本地上下文值:
|
||||
|
||||
```js
|
||||
store( 'myPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
const context = getContext();
|
||||
context.counter += 1;
|
||||
},
|
||||
updateName( event ) {
|
||||
const context = getContext();
|
||||
context.name = event.target.value;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
对本地上下文的修改将自动触发依赖该值的所有指令更新。
|
||||
|
||||
_请参阅[响应式与声明式思维](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md)指南,深入了解Interactivity API中的响应式工作原理。_
|
||||
|
||||
- **嵌套本地上下文**
|
||||
|
||||
本地上下文支持嵌套结构,子上下文可继承并覆盖父上下文的值:
|
||||
|
||||
```html
|
||||
<div data-wp-context='{ "theme": "light", "counter": 0 }'>
|
||||
<p>主题:<span data-wp-text="context.theme"></span></p>
|
||||
<p>计数器:<span data-wp-text="context.counter"></span></p>
|
||||
|
||||
<div data-wp-context='{ "theme": "dark" }'>
|
||||
<p>主题:<span data-wp-text="context.theme"></span></p>
|
||||
<p>计数器:<span data-wp-text="context.counter"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
此示例中,内层 `div` 将使用 `"dark"` 作为主题值,同时继承父上下文的计数器值 `0`。
|
||||
|
||||
# 理解全局状态、局部上下文与派生状态
|
||||
|
||||
Interactivity API 为创建交互式区块提供了强大的框架。要充分发挥其能力,关键在于理解何时使用全局状态、局部上下文或派生状态。本指南将阐明这些概念,并通过实际示例帮助您做出合适的选择。
|
||||
|
||||
首先简要定义全局状态、局部上下文和派生状态:
|
||||
|
||||
- **全局状态:** 页面中任何交互式区块均可访问和修改的全局数据,确保不同区块间保持同步。
|
||||
- **局部上下文:** 在 HTML 结构特定元素内定义的局部数据,仅该元素及其子元素可访问,为独立区块提供专属状态。
|
||||
- **派生状态:** 基于全局状态或局部上下文动态计算的数值,按需生成确保数据一致性,避免存储冗余数据。
|
||||
|
||||
接下来我们将深入探讨每个概念,并提供详细示例。
|
||||
|
||||
## 全局状态
|
||||
|
||||
Interactivity API 中的**全局状态**是指页面中任何交互式区块均可访问和修改的全局数据。它作为共享信息枢纽,使区块的不同部分能够通信并保持同步。无论交互式区块在 DOM 树中的位置如何,全局状态都是实现它们之间信息交换的理想机制。
|
||||
|
||||
在以下场景中应使用全局状态:
|
||||
|
||||
- 需要在 DOM 层级中无直接关联的多个交互式区块间共享数据
|
||||
- 希望为所有交互式区块维护统一数据源
|
||||
- 处理同时影响多个界面元素的数据
|
||||
- 需要实现页面级全局功能
|
||||
|
||||
### 全局状态操作指南
|
||||
|
||||
- **初始化全局状态**
|
||||
|
||||
通常应使用 `wp_interactivity_state` 函数在服务端定义初始全局状态值:
|
||||
|
||||
```php
|
||||
// 填充初始全局状态值
|
||||
wp_interactivity_state( 'myPlugin', array(
|
||||
'isDarkTheme' => true,
|
||||
'show' => false,
|
||||
'helloText' => __( 'world' ),
|
||||
));
|
||||
```
|
||||
|
||||
这些初始全局状态值将在 PHP 渲染页面时用于填充发送至浏览器的 HTML 标记。
|
||||
|
||||
- 开发者在 PHP 文件中编写的 HTML 标记:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-wp-interactive="myPlugin"
|
||||
data-wp-class--is-dark-theme="state.isDarkTheme"
|
||||
class="my-plugin"
|
||||
>
|
||||
<div data-wp-bind--hidden="!state.show">
|
||||
Hello <span data-wp-text="state.helloText"></span>
|
||||
</div>
|
||||
<button data-wp-on-async--click="actions.toggle">切换</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
- 指令处理完成后准备发送至浏览器的 HTML 标记:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-wp-interactive="myPlugin"
|
||||
data-wp-class--is-dark-theme="state.isDarkTheme"
|
||||
class="my-plugin is-dark-theme"
|
||||
>
|
||||
<div hidden data-wp-bind--hidden="!state.show">
|
||||
Hello <span data-wp-text="state.helloText">world</span>
|
||||
</div>
|
||||
<button data-wp-on-async--click="actions.toggle">切换</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
_请访问[服务端渲染指南](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md)了解指令在服务端处理的详细信息。_
|
||||
|
||||
若全局状态未在 PHP 页面渲染过程中使用,也可直接在客户端定义:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
state: {
|
||||
isLoading: false,
|
||||
},
|
||||
actions: {
|
||||
*loadSomething() {
|
||||
state.isLoading = true;
|
||||
// ...
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
_请注意,虽然这种方式可行,但通常建议在服务端定义所有全局状态。_
|
||||
|
||||
- **访问全局状态**
|
||||
|
||||
在 HTML 标记中,可通过在指令属性值中引用 `state` 直接访问全局状态值:
|
||||
|
||||
```html
|
||||
<div data-wp-bind--hidden="!state.show">
|
||||
<span data-wp-text="state.helloText"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
在 JavaScript 中,`@wordpress/interactivity` 包提供的 `store` 函数兼具设置器和获取器功能,返回指定命名空间的存储对象。
|
||||
|
||||
要在操作和回调中访问全局状态,可使用 `store` 函数返回对象的 `state` 属性:
|
||||
|
||||
```js
|
||||
const myPluginStore = store( 'myPlugin' );
|
||||
|
||||
myPluginStore.state; // 这是 'myPlugin' 命名空间的状态
|
||||
```
|
||||
|
||||
也可对 `store` 返回的对象进行解构:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin' );
|
||||
```
|
||||
|
||||
即使在定义存储时也可采用相同方式,这是最常见的使用场景:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
state: {
|
||||
// ...
|
||||
},
|
||||
actions: {
|
||||
toggle() {
|
||||
state.show = ! state.show;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
使用 `wp_interactivity_state` 函数在服务端初始化的全局状态也会自动包含在该对象中,因为它会从服务端自动序列化到客户端:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myPlugin', array(
|
||||
'someValue' => 1,
|
||||
));
|
||||
```
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
state: {
|
||||
otherValue: 2,
|
||||
},
|
||||
actions: {
|
||||
readGlobalState() {
|
||||
state.someValue; // 存在且初始值为 1
|
||||
state.otherValue; // 存在且初始值为 2
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
最后,所有对同一命名空间的 `store` 函数调用都会合并:
|
||||
|
||||
```js
|
||||
store( 'myPlugin', { state: { someValue: 1 } } );
|
||||
|
||||
store( 'myPlugin', { state: { otherValue: 2 } } );
|
||||
|
||||
/* 所有 `store` 调用都返回同一对象的稳定引用,
|
||||
* 因此可从任意调用中获取 `state` 引用 */
|
||||
const { state } = store( 'myPlugin' );
|
||||
|
||||
store( 'myPlugin', {
|
||||
actions: {
|
||||
readValues() {
|
||||
state.someValue; // 存在且初始值为 1
|
||||
state.otherValue; // 存在且初始值为 2
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
- **更新全局状态**
|
||||
|
||||
要更新全局状态,只需在从 `store` 函数获取 `state` 对象后对其进行修改:
|
||||
|
||||
```js
|
||||
const { state } = store( 'myPlugin', {
|
||||
actions: {
|
||||
updateValues() {
|
||||
state.someValue = 3;
|
||||
state.otherValue = 4;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
对全局状态的更改将自动触发依赖修改值的所有指令更新。
|
||||
|
||||
_请访问[响应式与声明式思维指南](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md)了解 Interactivity API 中响应式工作原理的详细信息。_
|
||||
@@ -0,0 +1,791 @@
|
||||
## 导入和导出类型化存储
|
||||
|
||||
在交互性 API 中,可以使用 `store` 函数访问来自其他命名空间的存储。
|
||||
|
||||
让我们回到 `todo-list` 区块示例,但这次假设 `add-post-to-todo` 区块属于不同的插件,因此将使用不同的命名空间。
|
||||
|
||||
```ts
|
||||
// 导入 `todo-list` 区块的存储
|
||||
const myTodoPlugin = store( 'myTodoPlugin' );
|
||||
|
||||
store( 'myAddPostToTodoPlugin', {
|
||||
actions: {
|
||||
addPostToTodo() {
|
||||
const todo = `阅读:${ state.postTitle }`.trim();
|
||||
if ( ! myTodoPlugin.state.todos.includes( todo ) ) {
|
||||
myTodoPlugin.actions.addTodo( todo );
|
||||
}
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
这在浏览器中运行正常,但 TypeScript 会提示 `myTodoPlugin.state` 和 `myTodoPlugin.actions` 未定义类型。
|
||||
|
||||
要解决这个问题,`myTodoPlugin` 插件可以导出带有正确类型调用的 `store` 函数结果,并通过脚本模块使其可用。
|
||||
|
||||
```ts
|
||||
// 导出已定义类型的状态和操作
|
||||
export const { state, actions } = store< TodoList >( 'myTodoPlugin', {
|
||||
// ...
|
||||
} );
|
||||
```
|
||||
|
||||
现在,`add-post-to-todo` 区块可以从 `myTodoPlugin` 脚本模块导入类型化存储,这不仅确保存储会被加载,还保证其包含正确的类型。
|
||||
|
||||
```ts
|
||||
import { store } from '@wordpress/interactivity';
|
||||
import {
|
||||
state as todoState,
|
||||
actions as todoActions,
|
||||
} from 'my-todo-plugin-module';
|
||||
|
||||
store( 'myAddPostToTodoPlugin', {
|
||||
actions: {
|
||||
addPostToTodo() {
|
||||
const todo = `阅读:${ state.postTitle }`.trim();
|
||||
if ( ! todoState.todos.includes( todo ) ) {
|
||||
todoActions.addTodo( todo );
|
||||
}
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
请记住,您需要将 `my-todo-plugin-module` 脚本模块声明为依赖项。
|
||||
|
||||
如果其他存储是可选的,并且您不希望急切加载它,可以使用动态导入代替静态导入。
|
||||
|
||||
```ts
|
||||
import { store } from '@wordpress/interactivity';
|
||||
|
||||
store( 'myAddPostToTodoPlugin', {
|
||||
actions: {
|
||||
*addPostToTodo() {
|
||||
const todoPlugin = yield import( 'my-todo-plugin-module' );
|
||||
const todo = `阅读:${ state.postTitle }`.trim();
|
||||
if ( ! todoPlugin.state.todos.includes( todo ) ) {
|
||||
todoPlugin.actions.addTodo( todo );
|
||||
}
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
在本指南中,我们探讨了为交互性 API 存储定义类型的不同方法,从自动推断类型到手动定义类型。我们还介绍了如何处理服务器初始化状态、本地上下文和派生状态,以及如何为异步操作定义类型。
|
||||
|
||||
请记住,选择自动推断类型还是手动定义类型取决于您的具体需求和存储的复杂性。无论选择哪种方法,TypeScript 都将帮助您构建更好、更可靠的交互式区块。
|
||||
|
||||
# 使用 TypeScript
|
||||
|
||||
Interactivity API 为 TypeScript 提供了强大的支持,使开发者能够构建类型安全的存储库,从而通过静态类型检查、改进的代码补全和简化的重构来提升开发体验。本指南将引导您完成在 Interactivity API 存储库中使用 TypeScript 的过程,涵盖从基础类型定义到处理复杂存储结构的高级技巧。
|
||||
|
||||
以下是 TypeScript 与 Interactivity API 交互的核心原则:
|
||||
|
||||
- **推断客户端类型**:当您使用 `store` 函数创建存储库时,TypeScript 会自动推断存储库属性(`state`、`actions` 等)的类型。这意味着您通常只需编写普通的 JavaScript 对象,TypeScript 会自动推断出类型。
|
||||
- **显式服务器类型**:当处理在服务器上定义的数据(如本地上下文或全局状态的初始值)时,您可以显式定义其类型,以确保所有内容都正确类型化。
|
||||
- **多存储部分**:即使您的存储库被拆分为多个部分,您也可以定义或推断每个部分的类型,然后将它们合并为一个代表整个存储库的类型。
|
||||
- **类型化外部存储库**:您可以从外部命名空间导入类型化的存储库,从而在类型安全的前提下使用其他插件的功能。
|
||||
|
||||
## 本地安装 `@wordpress/interactivity`
|
||||
|
||||
如果尚未安装,您需要在本地安装 `@wordpress/interactivity` 包,以便 TypeScript 可以在您的 IDE 中使用其类型。您可以使用以下命令进行安装:
|
||||
|
||||
`npm install @wordpress/interactivity`
|
||||
|
||||
保持该包的更新也是一个好习惯。
|
||||
|
||||
## 搭建新的类型化交互块
|
||||
|
||||
如果您想在本地环境中探索使用 TypeScript 的交互块示例,可以使用 `@wordpress/create-block-interactive-template`。
|
||||
|
||||
首先确保您的计算机上已安装 Node.js 和 `npm`。如果未安装,请查阅 [Node.js 开发环境](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/) 指南。
|
||||
|
||||
接下来,使用 [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) 包和 [`@wordpress/create-block-interactive-template`](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) 模板来搭建块。
|
||||
|
||||
选择您要创建插件的文件夹,在该文件夹的终端中执行以下命令,并在询问时选择 `typescript` 变体。
|
||||
|
||||
```
|
||||
npx @wordpress/create-block@latest --template @wordpress/create-block-interactive-template
|
||||
```
|
||||
|
||||
**重要提示**:不要在终端中提供 slug。否则,`create-block` 将不会询问您要选择哪个变体,而是默认选择非 TypeScript 变体。
|
||||
|
||||
最后,您可以继续按照 [入门指南](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/) 中的说明操作,其余说明保持不变。
|
||||
|
||||
## 类型化存储库
|
||||
|
||||
根据存储库的结构和您的偏好,您可以选择以下三种选项之一来生成存储库的类型:
|
||||
|
||||
1. 从客户端存储库定义推断类型。
|
||||
2. 手动类型化服务器状态,但其余部分从客户端存储库定义推断。
|
||||
3. 手动编写所有类型。
|
||||
|
||||
### 1. 从客户端存储库定义推断类型
|
||||
|
||||
当您使用 `store` 函数创建存储库时,TypeScript 会自动推断存储库属性(`state`、`actions`、`callbacks` 等)的类型。这意味着您通常只需编写普通的 JavaScript 对象,TypeScript 会自动推断出正确的类型。
|
||||
|
||||
让我们从一个计数器块的基础示例开始。我们将在块的 `view.ts` 文件中定义存储库,其中包含初始全局状态、一个操作和一个回调。
|
||||
|
||||
```ts
|
||||
// view.ts
|
||||
const myStore = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
myStore.state.counter += 1;
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
log() {
|
||||
console.log( `counter: ${ myStore.state.counter }` );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
如果您使用 TypeScript 检查 `myStore` 的类型,您会发现 TypeScript 已经正确推断出了类型。
|
||||
|
||||
```ts
|
||||
const myStore: {
|
||||
state: {
|
||||
counter: number;
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
};
|
||||
callbacks: {
|
||||
log(): void;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
您还可以解构 `state`、`actions` 和 `callbacks` 属性,类型仍然可以正确工作。
|
||||
|
||||
```ts
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
log() {
|
||||
console.log( `counter: ${ state.counter }` );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
总之,当您在单个 `store` 函数调用中定义了简单的存储库,并且不需要类型化任何在服务器上初始化的状态时,推断类型非常有用。
|
||||
|
||||
### 2. 手动编写服务器状态类型,其余类型从客户端存储定义推断
|
||||
|
||||
通过 `wp_interactivity_state` 函数在服务器端初始化的全局状态并不存在于客户端存储定义中,因此需要手动编写类型。但如果你不希望定义存储的所有类型,可以推断客户端存储定义的类型,并将其与服务器初始化状态的类型合并。
|
||||
|
||||
_请参阅[服务端渲染指南](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md)了解更多关于 `wp_interactivity_state` 以及指令在服务器端如何处理的信息。_
|
||||
|
||||
沿用之前的示例,将 `counter` 状态的初始化移至服务器端:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'counter' => 1,
|
||||
));
|
||||
```
|
||||
|
||||
接下来,定义服务器状态类型,并将其与从客户端存储定义推断的类型合并:
|
||||
|
||||
```ts
|
||||
// 定义服务器状态类型。
|
||||
type ServerState = {
|
||||
state: {
|
||||
counter: number;
|
||||
};
|
||||
};
|
||||
|
||||
// 将存储定义放入变量中,以便后续提取其类型。
|
||||
const storeDef = {
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
log() {
|
||||
console.log( `counter: ${ state.counter }` );
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 合并服务器状态和客户端存储定义的类型。
|
||||
type Store = ServerState & typeof storeDef;
|
||||
|
||||
// 调用 `store` 函数时注入最终类型。
|
||||
const { state } = store< Store >( 'myCounterPlugin', storeDef );
|
||||
```
|
||||
|
||||
或者,如果你不介意手动编写包括服务器端和客户端定义的所有状态类型,可以强制转换 `state` 属性,并让 TypeScript 推断存储的其余部分。
|
||||
|
||||
假设你在客户端全局状态中有一个名为 `product` 的额外属性:
|
||||
|
||||
```ts
|
||||
type State = {
|
||||
counter: number; // 服务器状态。
|
||||
product: number; // 客户端状态。
|
||||
};
|
||||
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
product: 2,
|
||||
} as State, // 手动强制转换整个状态。
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter * state.product;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
至此,TypeScript 将从存储定义中推断 `actions` 和 `callbacks` 属性的类型,但对于 `state` 属性,它将使用 `State` 类型,以确保包含客户端和服务器定义的正确类型。
|
||||
|
||||
总之,这种方法适用于需要手动编写服务器状态类型,但仍希望推断存储其余部分类型的情况。
|
||||
|
||||
### 3. 手动编写所有类型
|
||||
|
||||
如果你更倾向于手动定义存储的所有类型,而不是让 TypeScript 从客户端存储定义中推断,也可以这样做。只需将这些类型传递给 `store` 函数即可。
|
||||
|
||||
```ts
|
||||
// 定义存储类型。
|
||||
interface Store {
|
||||
state: {
|
||||
counter: number; // 初始服务器状态
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
};
|
||||
callbacks: {
|
||||
log(): void;
|
||||
};
|
||||
}
|
||||
|
||||
// 调用 `store` 函数时传递类型。
|
||||
const { state } = store< Store >( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
log() {
|
||||
console.log( `counter: ${ state.counter }` );
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
至此完成!总之,这种方法适用于你希望完全控制存储类型,并且不介意手动编写所有类型的情况。
|
||||
|
||||
## 为本地上下文编写类型
|
||||
|
||||
初始本地上下文是通过服务器端的 `data-wp-context` 指令定义的:
|
||||
|
||||
```html
|
||||
<div data-wp-context='{ "counter": 0 }'>...</div>
|
||||
```
|
||||
|
||||
因此,你需要手动定义其类型,并将其传递给 `getContext` 函数,以确保返回的属性具有正确的类型。
|
||||
|
||||
```ts
|
||||
// 定义上下文的类型。
|
||||
type MyContext = {
|
||||
counter: number;
|
||||
};
|
||||
|
||||
store( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
// 将其传递给 getContext 函数。
|
||||
const context = getContext< MyContext >();
|
||||
// 现在 `context` 已正确类型化。
|
||||
context.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
为了避免重复传递上下文类型,你还可以定义一个类型化函数,并使用该函数代替 `getContext`:
|
||||
|
||||
```ts
|
||||
// 定义上下文的类型。
|
||||
type MyContext = {
|
||||
counter: number;
|
||||
};
|
||||
|
||||
// 定义一个类型化函数。只需执行一次。
|
||||
const getMyContext = getContext< MyContext >;
|
||||
|
||||
store( 'myCounterPlugin', {
|
||||
actions: {
|
||||
increment() {
|
||||
// 使用你的类型化函数。
|
||||
const context = getMyContext();
|
||||
// 现在 `context` 已正确类型化。
|
||||
context.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
至此完成!现在你可以使用正确的类型访问上下文属性。
|
||||
|
||||
### 异步操作中的 yield 值类型标注
|
||||
|
||||
虽然 `AsyncAction<ReturnType>` 能够标注生成器函数及其最终返回值的类型,但生成器内部单个 `yield` 表达式解析的值仍可能被推断为 `any` 类型。
|
||||
|
||||
若需确保 `yield` 表达式解析值的类型正确(例如 `fetch` 调用或其他异步操作的结果),可使用 `TypeYield<T>` 辅助工具。该工具接收被 yield 的异步函数/操作的类型(`T`),并解析为 Promise 完成时返回值的类型。
|
||||
|
||||
假设 `fetchCounterData` 返回解析为对象的 Promise:
|
||||
|
||||
```ts
|
||||
import { store, type AsyncAction, type TypeYield } from '@wordpress/interactivity';
|
||||
|
||||
// 假设此函数在其他地方定义,用于获取特定数据
|
||||
const fetchCounterData = async ( counterValue: number ): Promise< { current: number, next: number } > => {
|
||||
// 内部逻辑...
|
||||
};
|
||||
|
||||
const { state, actions } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
*loadCounterData(): AsyncAction< void > {
|
||||
// 使用 TypeYield 正确标注 yield 解析值的类型
|
||||
const data = ( yield fetchCounterData( state.counter ) ) as TypeYield< typeof fetchCounterData >;
|
||||
|
||||
// 此时 data 已被正确标注为 { current: number, next: number }
|
||||
console.log( data.current, data.next );
|
||||
|
||||
// 根据获取的数据更新状态
|
||||
state.counter = data.next;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在此示例中,`( yield fetchCounterData( state.counter ) ) as TypeYield< typeof fetchCounterData >` 确保 `data` 常量被正确标注为 `{ current: number, next: number }`,与 `fetchCounterData` 的返回类型匹配。这使您可以安全地访问 `data.current` 和 `data.next` 等属性。
|
||||
|
||||
## 多模块存储的类型标注
|
||||
|
||||
有时存储会被拆分到不同文件中。当不同区块共享相同命名空间时,每个区块会加载其所需的存储部分。
|
||||
|
||||
请看两个区块的示例:
|
||||
|
||||
- `todo-list`:显示待办事项列表的区块
|
||||
- `add-post-to-todo`:显示按钮的区块,用于将包含“阅读 {$post_title}”文本的新待办项添加到列表
|
||||
|
||||
首先在服务端初始化 `todo-list` 区块的全局状态与派生状态。
|
||||
|
||||
```php
|
||||
<?php
|
||||
// todo-list-block/render.php
|
||||
$todos = array( 'Buy milk', 'Walk the dog' );
|
||||
wp_interactivity_state( 'myTodoPlugin', array(
|
||||
'todos' => $todos,
|
||||
'filter' => 'all',
|
||||
'filteredTodos' => $todos,
|
||||
));
|
||||
?>
|
||||
|
||||
<!-- HTML 标记... -->
|
||||
```
|
||||
|
||||
现在标注服务端状态并添加客户端存储定义。注意 `filteredTodos` 是派生状态,无需手动标注类型。
|
||||
|
||||
```ts
|
||||
// todo-list-block/view.ts
|
||||
type ServerState = {
|
||||
state: {
|
||||
todos: string[];
|
||||
filter: 'all' | 'completed';
|
||||
};
|
||||
};
|
||||
|
||||
const todoList = {
|
||||
state: {
|
||||
get filteredTodos(): string[] {
|
||||
return state.filter === 'completed'
|
||||
? state.todos.filter( ( todo ) => todo.includes( '✅' ) )
|
||||
: state.todos;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addTodo( todo: string ) {
|
||||
state.todos.push( todo );
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 将推断类型与服务端状态类型合并
|
||||
export type TodoList = ServerState & typeof todoList;
|
||||
|
||||
// 调用 `store` 函数时注入最终类型
|
||||
const { state } = store< TodoList >( 'myTodoPlugin', todoList );
|
||||
```
|
||||
|
||||
目前进展顺利。现在创建 `add-post-to-todo` 区块。
|
||||
|
||||
首先将当前文章标题添加到服务端状态。
|
||||
|
||||
```php
|
||||
<?php
|
||||
// add-post-to-todo-block/render.php
|
||||
wp_interactivity_state( 'myTodoPlugin', array(
|
||||
'postTitle' => get_the_title(),
|
||||
));
|
||||
?>
|
||||
|
||||
<!-- HTML 标记... -->
|
||||
```
|
||||
|
||||
现在标注该服务端状态并添加客户端存储定义。
|
||||
|
||||
```ts
|
||||
// add-post-to-todo-block/view.ts
|
||||
type ServerState = {
|
||||
state: {
|
||||
postTitle: string;
|
||||
};
|
||||
};
|
||||
|
||||
const addPostToTodo = {
|
||||
actions: {
|
||||
addPostToTodo() {
|
||||
const todo = `阅读:${ state.postTitle }`.trim();
|
||||
if ( ! state.todos.includes( todo ) ) {
|
||||
actions.addTodo( todo );
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 将推断类型与服务端状态类型合并
|
||||
type Store = ServerState & typeof addPostToTodo;
|
||||
|
||||
// 调用 `store` 函数时注入最终类型
|
||||
const { state, actions } = store< Store >( 'myTodoPlugin', addPostToTodo );
|
||||
```
|
||||
|
||||
这在浏览器中运行正常,但 TypeScript 会提示此区块中缺少 `state.todos` 和 `actions.addtodo`。
|
||||
|
||||
要解决此问题,需要从 `todo-list` 区块导入 `TodoList` 类型并与其他类型合并。
|
||||
|
||||
```ts
|
||||
import type { TodoList } from '../todo-list-block/view';
|
||||
|
||||
// ...
|
||||
|
||||
// 将推断类型与服务端状态类型合并
|
||||
type Store = TodoList & ServerState & typeof addPostToTodo;
|
||||
```
|
||||
|
||||
现在 TypeScript 将识别 `state.todos` 和 `actions.addTodo` 在 `add-post-to-todo` 区块中的可用性。
|
||||
|
||||
这种方法使 `add-post-to-todo` 区块能与现有待办列表交互,同时保持类型安全并为共享存储添加自身功能。
|
||||
|
||||
若需在 `todo-list` 区块中使用 `add-post-to-todo` 的类型,只需导出其类型并导入到另一个 `view.ts` 文件中。
|
||||
|
||||
最后,如果希望手动定义所有类型而非推断,可在单独文件中定义类型,并将该定义导入到每个存储部分。以下是待办列表示例的实现方式:
|
||||
|
||||
```ts
|
||||
// types.ts
|
||||
interface Store {
|
||||
state: {
|
||||
todos: string[];
|
||||
filter: 'all' | 'completed';
|
||||
filtered: string[];
|
||||
postTitle: string;
|
||||
};
|
||||
actions: {
|
||||
addTodo( todo: string ): void;
|
||||
addPostToTodo(): void;
|
||||
};
|
||||
}
|
||||
|
||||
export default Store;
|
||||
```
|
||||
|
||||
```ts
|
||||
// todo-list-block/view.ts
|
||||
import type Store from '../types';
|
||||
|
||||
const { state } = store< Store >( 'myTodoPlugin', {
|
||||
// 此处所有内容均正确标注类型
|
||||
} );
|
||||
```
|
||||
|
||||
```ts
|
||||
// add-post-to-todo-block/view.ts
|
||||
import type Store from '../types';
|
||||
|
||||
const { state, actions } = store< Store >( 'myTodoPlugin', {
|
||||
// 此处所有内容均正确标注类型
|
||||
} );
|
||||
```
|
||||
|
||||
这种方法使您能完全掌控类型,并确保存储所有部分的一致性。当存储结构复杂或需要在多个区块/组件中强制实施特定接口时,此方法尤其有用。
|
||||
|
||||
## 派生状态的类型定义
|
||||
|
||||
派生状态是基于全局状态或局部上下文计算得出的数据。在客户端存储定义中,它通过 `state` 对象中的 getter 来定义。
|
||||
|
||||
_请访问[理解全局状态、局部上下文和派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)指南,了解更多关于交互性 API 中派生状态的工作原理。_
|
||||
|
||||
延续之前的示例,我们创建一个派生状态,使其值为计数器值的两倍。
|
||||
|
||||
```ts
|
||||
type MyContext = {
|
||||
counter: number;
|
||||
};
|
||||
|
||||
const myStore = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
get double() {
|
||||
const { counter } = getContext< MyContext >();
|
||||
return counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1; // 此处的类型为 number。
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
通常情况下,当派生状态依赖于局部上下文时,TypeScript 能够推断出正确的类型:
|
||||
|
||||
```ts
|
||||
const myStore: {
|
||||
state: {
|
||||
readonly double: number;
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
但当派生状态的返回值直接依赖于全局状态的某一部分时,TypeScript 将无法推断类型,因为它会声称存在循环引用。
|
||||
|
||||
例如,在这种情况下,TypeScript 无法推断 `state.double` 的类型,因为它依赖于 `state.counter`,而 `state` 的类型在 `state.double` 的类型定义完成之前无法确定,从而形成了循环引用。
|
||||
|
||||
```ts
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
get double() {
|
||||
// TypeScript 无法推断此返回类型,因为它依赖于 `state`。
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1; // 此处的类型现在为 unknown。
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在这种情况下,根据你的 TypeScript 配置,TypeScript 可能会警告你存在循环引用,或者直接将 `any` 类型赋给 `state` 属性。
|
||||
|
||||
然而,解决这个问题很简单;我们只需要手动为 TypeScript 提供该 getter 的返回类型。一旦我们这样做,循环引用就会消失,TypeScript 将能够再次推断所有 `state` 类型。
|
||||
|
||||
```ts
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 1,
|
||||
get double(): number {
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1; // 正确推断!
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
现在,以下是之前存储的正确推断类型。
|
||||
|
||||
```ts
|
||||
const myStore: {
|
||||
state: {
|
||||
counter: number;
|
||||
readonly double: number;
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
在服务器端使用 `wp_interactivity_state` 时,请记住你还需要定义派生状态的初始值,如下所示:
|
||||
|
||||
```php
|
||||
wp_interactivity_state( 'myCounterPlugin', array(
|
||||
'counter' => 1,
|
||||
'double' => 2,
|
||||
));
|
||||
```
|
||||
|
||||
但如果你正在推断类型,则无需手动定义派生状态的类型,因为它已经存在于你的客户端存储定义中。
|
||||
|
||||
```ts
|
||||
// 你不需要在此处定义 `state.double` 的类型。
|
||||
type ServerState = {
|
||||
state: {
|
||||
counter: number;
|
||||
};
|
||||
};
|
||||
|
||||
// `state.double` 的类型从此处推断。
|
||||
const storeDef = {
|
||||
state: {
|
||||
get double(): number {
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 合并服务器状态和客户端存储定义的类型。
|
||||
type Store = ServerState & typeof storeDef;
|
||||
|
||||
// 在调用 `store` 函数时注入最终类型。
|
||||
const { state } = store< Store >( 'myCounterPlugin', storeDef );
|
||||
```
|
||||
|
||||
就是这样!现在你可以使用正确的类型访问派生状态属性。
|
||||
|
||||
## 异步操作的类型定义
|
||||
|
||||
在使用 TypeScript 与交互性 API 时,另一件需要注意的事情是,异步操作必须使用生成器而不是异步函数来定义。
|
||||
|
||||
在交互性 API 的异步操作中使用生成器的原因是,能够在异步操作在 yield 后继续执行时,恢复最初触发操作的作用域。但这只是语法上的改变,**除此之外,这些函数的操作方式与常规异步函数完全相同**,并且 `store` 函数推断出的类型反映了这一点。
|
||||
|
||||
延续之前的示例,我们向存储添加一个异步操作。
|
||||
|
||||
```ts
|
||||
const { state } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
get double(): number {
|
||||
return state.counter * 2;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
increment() {
|
||||
state.counter += 1;
|
||||
},
|
||||
*delayedIncrement() {
|
||||
yield new Promise( ( r ) => setTimeout( r, 1000 ) );
|
||||
state.counter += 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
此存储的推断类型为:
|
||||
|
||||
```ts
|
||||
const myStore: {
|
||||
state: {
|
||||
counter: number;
|
||||
readonly double: number;
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
// 此处的行为类似于常规异步函数。
|
||||
delayedIncrement(): Promise< void >;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
这也意味着你可以在外部函数中使用异步操作,并且 TypeScript 将正确使用异步函数类型。
|
||||
|
||||
```ts
|
||||
const someAsyncFunction = async () => {
|
||||
// 这可以正常工作,并且类型正确。
|
||||
await actions.delayedIncrement( 2000 );
|
||||
};
|
||||
```
|
||||
|
||||
当你不推断类型而是手动为整个存储编写类型时,可以为异步操作使用异步函数类型。
|
||||
|
||||
```ts
|
||||
type Store = {
|
||||
state: {
|
||||
counter: number;
|
||||
readonly double: number;
|
||||
};
|
||||
actions: {
|
||||
increment(): void;
|
||||
delayedIncrement(): Promise< void >; // 你可以在此处使用异步函数。
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
在使用异步操作时,需要注意一点。与派生状态类似,如果异步操作在 `yield` 表达式中使用 `state`(例如,将 `state` 传递给一个随后被 yield 的异步函数),或者其返回值依赖于 `state`,TypeScript 可能由于潜在的循环引用而无法正确推断类型。
|
||||
|
||||
```ts
|
||||
const { state, actions } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
*delayedOperation() {
|
||||
// 示例:state.counter 被用作 yield 逻辑的一部分。
|
||||
yield fetchCounterData( state.counter );
|
||||
|
||||
// 并且/或者最终返回值依赖于 state。
|
||||
return state.counter + 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
在这种情况下,TypeScript 可能会发出关于循环引用的警告,或者默认使用 `any` 类型。为了解决这个问题,你需要手动为生成器函数定义类型。交互性 API 为此提供了一个辅助类型 `AsyncAction<ReturnType>`。
|
||||
|
||||
```ts
|
||||
import { store, type AsyncAction } from '@wordpress/interactivity';
|
||||
|
||||
const { state, actions } = store( 'myCounterPlugin', {
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
*delayedOperation(): AsyncAction< number > {
|
||||
// 现在,这不会导致循环引用。
|
||||
yield fetchCounterData( state.counter );
|
||||
|
||||
// 现在,类型正确。
|
||||
return state.counter + 1;
|
||||
},
|
||||
},
|
||||
} );
|
||||
```
|
||||
|
||||
就是这样!`AsyncAction<ReturnType>` 辅助类型被定义为 `Generator<any, ReturnType, unknown>`。通过使用 `any` 作为生成器 yield 的值的类型,它有助于打破循环引用,使得 TypeScript 能够在 `state` 参与 `yield` 表达式或最终返回值时正确推断类型。你只需要指定异步操作的最终 `ReturnType` 即可。
|
||||
193
docs/reference-guides/interactivity-api/iapi-about.md
Normal file
193
docs/reference-guides/interactivity-api/iapi-about.md
Normal file
@@ -0,0 +1,193 @@
|
||||
### 原子化与可组合性
|
||||
|
||||
每个指令控制DOM的一小部分,您可以组合多个指令来创建丰富、交互式的用户体验。
|
||||
|
||||
### 兼容现有区块开发工具
|
||||
|
||||
该API可直接与标准区块构建工具(如[`wp-scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/))配合使用。要使`wp-scripts`正确构建使用交互式API的[脚本模块](https://make.wordpress.org/core/2024/03/04/script-modules-in-6-5/),只需在[`build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build)和[`start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start)脚本中使用`--experimental-modules`标志即可。
|
||||
|
||||
### 客户端导航
|
||||
|
||||
交互式API内置了为站点添加客户端导航的基础功能。此功能完全可选,但它使得在无需脱离WordPress渲染系统的情况下创建此类用户体验成为可能。
|
||||
|
||||
<div class="callout callout-info">
|
||||
基于交互式API的全页客户端导航仍在开发中(参见<a href="https://github.com/WordPress/gutenberg/issues/60951">#60951</a>)。但预计所有交互区块都需要使用交互式API来实现全页客户端导航。仅在此情况下,交互式API可能无法完全兼容其他库(如jQuery)。
|
||||
</div>
|
||||
|
||||
该API还能与[视图过渡API](https://developer.chrome.com/docs/web-platform/view-transitions/)完美配合,让开发者轻松实现页面转场动画。
|
||||
|
||||
## 为何要建立标准?
|
||||
|
||||
使用交互式API的区块与使用jQuery等其他方案的交互区块可以共存,且所有功能均能正常运作。但交互式API为交互区块带来了独特优势:
|
||||
|
||||
- **区块间可轻松通信**:采用标准后,区块通信默认即可实现。若不同区块使用不同的前端交互方案,当由不同开发者创建区块时,跨区块通信会变得复杂且几乎无法实现
|
||||
- **组合性与兼容性**:您可以组合交互区块,并将其嵌套至具有定义行为的结构中。遵循同一标准确保了它们的完全跨兼容性。若每个区块采用不同交互方案,很可能导致功能冲突
|
||||
- **减少浏览器加载量**:若每个插件作者使用不同JS框架,前端将加载更多代码。统一标准可实现代码复用
|
||||
- 当页面所有区块采用此标准时,**可启用全站级功能(如客户端导航)**
|
||||
|
||||
此外,通过建立标准,**WordPress可为开发者承担最大程度的复杂性**——系统将处理创建交互区块所需的大部分工作。
|
||||
|
||||
_标准所吸纳的复杂性_
|
||||
|
||||
<img alt="双列对比表展示有无标准时的差异。无标准时区块开发者需处理所有事项;有标准时:完全由标准处理——工具链、水合作用、与WordPress集成、交互部件服务端渲染、区块间通信、前端性能;部分由标准处理——安全性、无障碍访问、最佳实践;开发者负责——区块逻辑。无标准列中所有事项均需开发者承担" width=60% src="https://make.wordpress.core/files/2023/03/standard-graph.png">
|
||||
|
||||
这种复杂性吸收机制降低了对开发知识的要求,使开发者无需纠结过多技术决策。
|
||||
|
||||
采用标准后,从其他交互区块学习变得简单,促进了协作与代码复用。最终使开发流程更精简,对经验较少的开发者更友好。
|
||||
|
||||
### 向后兼容
|
||||
|
||||
由于交互性 API 与服务器端渲染完美配合,您可以继续使用所有 WordPress API,包括:
|
||||
|
||||
- **WordPress 过滤器和操作**:您可以继续使用 WordPress 钩子来修改 HTML,甚至修改指令。此外,现有钩子将按预期正常工作。
|
||||
- **核心翻译 API**:例如 `__()` 和 `_e()`。您可以用它来翻译 HTML 中的文本(像往常一样),甚至可以在指令的服务器端使用这些 API。
|
||||
|
||||
### 可选且渐进式采用
|
||||
|
||||
交互性 API 流程建立在 WordPress 坚实的基础上和模式之上,推动**渐进式增强**。
|
||||
|
||||
例如,带有指令的块可以与其他(交互式或非交互式)块共存。这意味着如果页面上有其他使用 jQuery 等其他框架的块,所有内容都将按预期工作。
|
||||
|
||||
<div class="callout callout-warning">
|
||||
使用交互性 API 进行全页面客户端导航将是与其他库兼容规则的例外。详情请参阅 <a href="#客户端导航">客户端导航</a>。
|
||||
</div>
|
||||
|
||||
### 声明式与响应式
|
||||
|
||||
交互性 API 遵循与其他流行 JS 框架类似的方法,通过分离状态、操作和回调并以声明方式定义它们。为什么是声明式?
|
||||
|
||||
声明式代码描述程序**应该做什么**,而命令式代码描述程序**应该如何做**。使用声明式方法,UI 会自动响应底层数据的变化而更新。使用命令式方法,您必须在数据每次变化时手动更新 UI。比较以下两个代码示例:
|
||||
|
||||
_命令式代码_
|
||||
|
||||
```html
|
||||
<button id="toggle-button">切换元素</button>
|
||||
<p>此元素现在可见!</p>
|
||||
<script>
|
||||
const button = document.getElementById("toggle-button");
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
const element = document.getElementById("element");
|
||||
if(element) {
|
||||
element.remove();
|
||||
} else {
|
||||
const newElement = document.createElement("p");
|
||||
newElement.textContent = "此元素可见";
|
||||
document.body.appendChild(newElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
_声明式代码_
|
||||
|
||||
这是上面分享的相同用例,但作为使用此新系统的声明式代码示例。JavaScript 逻辑定义在块的 `view.js` 文件中,并在 `render.php` 中将指令添加到标记中。
|
||||
|
||||
```js
|
||||
// view.js 文件
|
||||
|
||||
import { store, getContext } from "@wordpress/interactivity";
|
||||
|
||||
store( 'wpmovies', {
|
||||
actions: {
|
||||
toggle: () => {
|
||||
const context = getContext();
|
||||
context.isOpen = !context.isOpen;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```php
|
||||
<!-- Render.php 文件 -->
|
||||
|
||||
<div
|
||||
data-wp-interactive='wpmovies'
|
||||
<?php echo wp_interactivity_data_wp_context( array( 'isOpen' => true ) ); ?>
|
||||
>
|
||||
<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>
|
||||
```
|
||||
|
||||
在创建简单的用户体验时,使用命令式代码可能更容易,但随着应用程序变得更加复杂,它会变得困难得多。交互性 API 必须涵盖从最简单到最具挑战性的所有用例。这就是为什么使用指令的声明式方法更适合交互性 API。
|
||||
|
||||
### 高性能
|
||||
|
||||
该 API 旨在尽可能高效:
|
||||
|
||||
- **指令所需的运行时代码仅约 10 KB**,并且只需为所有块加载一次。
|
||||
- **脚本加载不会阻塞页面渲染**。
|
||||
|
||||
### 可扩展
|
||||
|
||||
指令可以直接从 HTML 中添加、删除或修改。例如,用户可以使用 [`render_block` 过滤器](https://developer.wordpress.org/reference/hooks/render_block/)来修改 HTML 及其行为。
|
||||
|
||||
# 关于交互性 API
|
||||
|
||||
交互性 API 是**一套基于声明式代码的[指令系统](#为什么采用指令),用于[为区块添加前端交互功能](#api-目标)**的[标准化方案](#为什么需要标准)。
|
||||
|
||||
**指令通过特殊属性扩展 HTML**,告知交互性 API 为 DOM 元素附加指定行为甚至进行元素转换。若您熟悉 [Alpine.js](https://alpinejs.dev/),会发现这是类似的实现方式,但本 API 是专为与 WordPress 无缝协作而设计的。
|
||||
|
||||
## API 目标
|
||||
|
||||
交互性 API 的主要目标是**为 Gutenberg 区块的前端交互提供标准化且简单的处理方式**。
|
||||
|
||||
标准化能让**开发者更轻松地创建丰富的交互式用户体验**,无论是简单的计数器或弹窗,还是更复杂的即时页面导航、即时搜索、购物车与结算功能。
|
||||
|
||||
目前即使不借助交互性 API,这些用户体验在技术上也能实现。但随着用户体验复杂度的提升以及区块间交互的增多,开发者构建和维护网站的难度会大幅增加。他们需要自行解决大量技术挑战。本 API 旨在为这类交互需求提供开箱即用的解决方案。
|
||||
|
||||
为应对这些挑战,我们为交互性 API 设定了以下要求/目标:
|
||||
|
||||
- **区块优先与 PHP 优先**:API 必须与 PHP 及当前区块系统良好兼容(包括在 WordPress 中被广泛使用的动态区块),必须支持服务端渲染,且服务端渲染的 HTML 与客户端水合的 HTML 必须完全一致——这对 SEO 和用户体验至关重要
|
||||
- **向后兼容**:API 必须兼容 WordPress 钩子机制(例如可修改服务端渲染的 HTML),同时需支持国际化特性并与站点现有 JS 库(如 jQuery)兼容
|
||||
- **可选性与渐进式采用**:延续前一点,API 必须保持可选特性,支持渐进式采用——即未使用本 API 的交互区块可与使用本 API 的区块共存
|
||||
- **声明式与响应式**:API 需采用声明式代码,监听数据变化,并仅更新依赖该数据的 DOM 部分
|
||||
- **高性能**:运行时必须快速轻量,以确保最佳用户体验
|
||||
- **可扩展性**:如同 WordPress 始终注重扩展性,新系统必须提供扩展模式以覆盖大多数使用场景
|
||||
- **原子化与可组合性**:通过小型可复用部件的组合构建复杂系统,是创建灵活可扩展解决方案的必要条件
|
||||
- **兼容现有区块开发工具**:API 必须与现有区块构建工具集成,无需开发者配置额外工具
|
||||
|
||||
除上述要求外,在任何解决方案之上集成**客户端导航**都应简单高效。客户端导航是实现页面间跳转无需整页刷新的技术,正是网页开发者最迫切需求的用户体验之一。因此该功能必须与新系统保持兼容。
|
||||
|
||||
## 为什么采用指令?
|
||||
|
||||
指令是[对各种可行方案深度研究](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-faq/#what-approaches-have-been-considered-instead-of-using-directives)的成果。我们发现这种设计能最高效地满足需求。
|
||||
|
||||
### 区块优先与 PHP 友好
|
||||
|
||||
本 API 专为区块生态设计,并秉承 WordPress 恪守 Web 标准的历史传统。
|
||||
|
||||
由于指令以 HTML 属性形式存在,它们天然适合动态区块与 PHP 环境。
|
||||
|
||||
*动态区块示例*
|
||||
```html
|
||||
<div
|
||||
data-wp-interactive='wpmovies'
|
||||
<?php echo wp_interactivity_data_wp_context( array( '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>
|
||||
```
|
||||
|
||||
如您所见,[`data-wp-on--click`](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#wp-on) 或 [`data-wp-bind--hidden`](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#wp-bind) 等指令以自定义 HTML 属性形式添加。WordPress 可在服务端处理此 HTML,执行指令逻辑并生成对应标记。
|
||||
133
docs/reference-guides/interactivity-api/iapi-faq.md
Normal file
133
docs/reference-guides/interactivity-api/iapi-faq.md
Normal file
@@ -0,0 +1,133 @@
|
||||
## 是否支持自定义安全策略?
|
||||
|
||||
是的。交互式API不使用 [`eval()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval) 或 [`Function()`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/Function) 构造函数,因此不会违反 [`unsafe-eval`](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Security-Policy#unsafe_keyword_values) 内容安全策略。该API在设计上可与任何[自定义内容安全策略](https://developer.wordpress.org/apis/security/)协同工作。
|
||||
|
||||
## 能否使用指令发起AJAX/REST-API请求?
|
||||
|
||||
当然可以。通过指令调用的操作和回调函数可以执行任何JavaScript函数能实现的功能,包括发起API请求。
|
||||
|
||||
## 交互性API能否在区块之外使用?
|
||||
|
||||
当然可以,它不仅限于区块。虽然你会看到许多关于交互性API如何为创建交互式区块提供标准的讨论,但这只是因为那是最常见的用例。更广义地说,交互性API标准可用于为WordPress任何部分的前端添加“交互行为”。
|
||||
|
||||
有关在区块外使用任意HTML的交互性API的详细信息,请参阅[`wp_interactivity_process_directives`函数](https://developer.wordpress.org/reference/functions/wp_interactivity_process_directives/)。
|
||||
|
||||
## 这是否意味着我必须将所有交互式区块迁移到此API?
|
||||
|
||||
不需要。未使用交互性API的区块可以与使用该API的区块共存。但如前所述,请注意使用该API的区块具有以下优势:
|
||||
|
||||
- **区块间可轻松通信**:采用标准后,这种通信默认即可实现。当不同区块使用不同方法实现前端交互时,区块间通信会变得复杂,尤其在由不同开发者创建区块时几乎无法实现。
|
||||
- **可组合性与兼容性**:你可以组合交互式区块,将其嵌套在具有定义行为的结构中,并且由于遵循相同标准,它们完全跨兼容。如果每个区块采用不同的交互方法,很可能会导致功能冲突。
|
||||
- **减少向浏览器传输的数据量**:如果每个插件作者使用不同的JS框架,前端将加载更多代码。如果所有区块使用同一框架,代码可实现复用。
|
||||
- 若页面所有区块都采用此标准,则可启用站点级功能(如客户端导航)。
|
||||
|
||||
## 使用此API对性能有何影响?对于非常简单的用例,加载交互性API是否值得?
|
||||
|
||||
该API在设计时已充分考虑性能因素:
|
||||
|
||||
- **指令所需的运行时代码仅约10KB**,且所有区块只需加载一次。
|
||||
- **所有属于交互性API的脚本模块(包括`view.js`文件)均不会阻塞页面渲染。**
|
||||
- 目前正在[持续探索](https://github.com/WordPress/gutenberg/discussions/52723)**在区块进入视口后延迟加载脚本**的可能性。通过这种方式,可在不影响用户体验的前提下优化初始加载。
|
||||
|
||||
## 是否与核心翻译API兼容?
|
||||
|
||||
由于交互性API与服务器端渲染完美配合,你可以使用所有WordPress API,包括[`__()`](https://developer.wordpress.org/reference/functions/__/)和[`_e()`](https://developer.wordpress.org/reference/functions/_e/)。你可以用它翻译HTML中的文本(就像通常做法),甚至可以在[服务器端使用`wp_interactivity_state()`](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#setting-the-store)时在存储库中使用。示例如下:
|
||||
|
||||
```php
|
||||
// render.php
|
||||
wp_interactivity_state( 'favoriteMovies', array(
|
||||
"1" => array(
|
||||
"id" => "123-abc",
|
||||
"movieName" => __("影片名称", "textdomain")
|
||||
),
|
||||
) );
|
||||
```
|
||||
|
||||
目前正在开发与脚本模块(交互性API所需)兼容的翻译API。请关注[#60234](https://core.trac.wordpress.org/ticket/60234)了解进展。
|
||||
|
||||
## 我担心XSS攻击;JavaScript能否被注入到指令中?
|
||||
|
||||
不会。交互性API只允许将[引用](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#values-of-directives-are-references-to-store-properties)作为值传递给指令。这种方式无需eval()完整的JavaScript表达式,因此不可能实施XSS攻击。
|
||||
|
||||
### 原生 JavaScript
|
||||
|
||||
请参阅下方答案。
|
||||
|
||||
### 模板领域特定语言
|
||||
|
||||
我们还研究了创建用于编写交互式模板的[领域特定语言](https://en.wikipedia.org/wiki/Domain-specific_language)的可能性。使用该模板DSL编写的代码将被编译为JavaScript和PHP。然而,创建生产级的模板编译器十分复杂,需要投入大量精力且存在较高风险。未来仍会考虑采用这种方案,届时指令系统将作为编译目标。
|
||||
|
||||
## 作为区块开发者,为何应选择交互式API而非React?
|
||||
|
||||
在PHP服务端渲染环境中,前端使用React无法流畅运行。所有基于React渲染区块的方案都需通过客户端JavaScript加载内容。若仅通过客户端渲染区块,通常会导致用户体验不佳——用户需要盯着空白占位符和加载动画等待内容呈现。
|
||||
|
||||
虽然可通过PHP扩展(如v8js)运行JS,但遗憾的是PHP扩展不具向后兼容性,且必须存在PHP降级方案时才能使用。
|
||||
|
||||
当前虽可实现在PHP中服务端渲染区块,并同时在前端使用React渲染相同区块,但这会导致开发体验恶化——相同逻辑需在PHP和React部分重复实现。更严重的是,您将面临WordPress钩子引发的隐蔽错误!
|
||||
|
||||
假设安装的第三方插件通过钩子(过滤器)修改了服务端渲染的HTML,例如为区块HTML添加单个CSS类。该CSS类会存在于服务端渲染的标记中。但在前端,您的区块通过React重新渲染时,由于无法对React渲染内容应用WordPress钩子,最终呈现的内容将缺失这个CSS类!
|
||||
|
||||
反观交互式API,其设计理念是通过指令为服务端渲染的HTML增强交互行为,因此能与WordPress钩子完美协同。这也意味着它可以开箱即用地兼容WordPress后端API(如i18n)。
|
||||
|
||||
总结而言,选择交互式API而非单纯使用React具有以下优势:
|
||||
|
||||
- 使用React时,交互式区块在客户端生成的标记必须与服务端PHP生成的完全一致。而交互式API无此限制,指令可直接附加于服务端渲染的HTML
|
||||
- 交互式API对PHP更友好,可无缝兼容WordPress钩子及其他服务端功能(如国际化)。例如使用React时,您无法预知服务端应用的钩子,其修改效果将在水合后被覆盖
|
||||
- 具备[采用标准化方案](/docs/reference-guides/interactivity-api/iapi-about.md#why-a-standard)的所有优势
|
||||
|
||||
## 与jQuery或原生JavaScript相比,交互式API的优势何在?
|
||||
|
||||
核心差异在于交互式API具备**声明式与响应式特性**,这使得编写和维护复杂交互体验变得更为轻松。此外,它**专为区块开发量身定制**,提供的标准规范具备前述各项优势,包括区块间通信、兼容性以及客户端导航等全站功能。
|
||||
|
||||
最后与jQuery对比:**交互式API运行时仅约10kb**,体积更轻量化。实际上,WordPress生态正在持续推进移除jQuery等重型框架的工作,本API将为此提供助力。
|
||||
|
||||
## 是否需要同时掌握React、PHP和本交互式API?
|
||||
|
||||
若希望使用本API为区块添加前端交互功能,简短答案是肯定的。如果区块无需交互功能,区块创建流程将完全保持不变。
|
||||
|
||||
交互式API引入了新的标准方法,以简化WordPress前端交互行为的集成。这意味着您仍需要使用React来处理区块的编辑器部分。
|
||||
|
||||
反之,若要创建交互式区块,采用交互式API后您无需处理复杂议题,例如工具链配置、WordPress集成、区块间通信或交互部件的服务端渲染。
|
||||
|
||||
# 常见问题解答
|
||||
|
||||
## 交互性 API 底层是如何运作的?
|
||||
|
||||
其三大核心组件包括:
|
||||
|
||||
- 采用 [Preact](https://preactjs.com/) 与 [Preact Signals](https://preactjs.com/guide/v10/signals/) 组合实现水合作用、客户端逻辑及客户端导航功能
|
||||
- 可被客户端与服务端共同解析的 HTML 指令系统
|
||||
- 由 [HTML 标签处理器](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) 处理的服务器端逻辑
|
||||
|
||||
## 为何选择 Preact 构建指令系统?为何不采用 React 或其他 JavaScript 框架?
|
||||
|
||||
在前端应用场景中(这也是交互性 API 的核心定位),Preact 相较于 React 及 Vue、Svelte、Solid 等框架具有显著优势:
|
||||
|
||||
- 体积轻量:包含 [钩子函数](https://preactjs.com/guide/v10/hooks/) 与 [信号系统](https://preactjs.com/blog/introducing-signals/) 仅 8kB
|
||||
- 开箱即用的 DOM 差异化比较功能
|
||||
- 通过可选钩子实现高度可扩展性,该特性被广泛应用于钩子函数、React 兼容层 (preact/compat) 及信号系统 (@preact/signals),覆盖除 DOM 差异算法外的所有功能模块
|
||||
- 核心团队技术实力雄厚且提供强力支持,并对 Preact 的“孤岛架构”应用模式表现出浓厚兴趣
|
||||
|
||||
## Gutenberg 编辑器是否会因交互性 API 采用 Preact 而从 React 迁移?
|
||||
|
||||
不会。目前暂无迁移计划。作为一个完全交互式应用,编辑器的功能需求与优势考量存在显著差异。虽然 Preact 提供完全兼容 React 生态的 [`@preact/compat`](https://preactjs.com/guide/v10/switching-to-preact/) 套件,且已被众多大型网络应用采纳,但在区块编辑器中使用 Preact 无法获得像交互性 API 在前端场景中的同等优势。
|
||||
|
||||
## 除指令系统外,还考虑过哪些替代方案?
|
||||
|
||||
我们曾评估多种替代方案,以下是部分方案的简要概述:
|
||||
|
||||
### React 及其他 JavaScript 框架
|
||||
|
||||
由于 Gutenberg 开发者对 React 更熟悉,该框架成为首要考量对象。其他主流 JS 框架如 Svelte、Vue.js 或 Angular 也在评估之列,但这些框架(包括 React)均存在与 PHP 环境兼容性不足、无法适配 WordPress 钩子系统或国际化需求的局限性。
|
||||
|
||||
### Alpine.js
|
||||
|
||||
Alpine.js 是优秀的框架方案,其诸多功能设计为交互性 API 带来重要启发。但该框架不支持服务端渲染 [指令](https://github.com/alpinejs/alpine/tree/d7f9d641f7a763c56c598d118bd189a406a22383/packages/docs/src/en/directives),而为 WordPress 区块量身打造专属系统能带来更多收益。
|
||||
|
||||
最终选择 Preact 取代 Alpine.js 基于多重考量:更小的体积尺寸、更优的性能表现(特别是集成 [信号系统](https://preactjs.com/guide/v10/signals/) 后)、基于 Preact 声明式语法及工具链(钩子、信号)编写自定义指令、相比 Alpine.js 具有更充分的实战检验与更庞大的开发者社区。同时完美兼容 React(便于共享编辑器中的客户端渲染组件),并为交互性 API 提供开箱即用的最快 DOM 差异算法,包含 UI 状态保持功能。
|
||||
|
||||
更重要的是,依托 Preact 的底层支撑,交互性 API 可专注管理“最终呈现层”,从而更好适配 WordPress 的特殊需求。例如:为避免安全风险并确保符合严格的安全策略,指令内部禁止使用 JavaScript 表达式;所有 WordPress 指令均符合 HTML 属性规范标准。
|
||||
|
||||
<div class="callout callout-info">
|
||||
欢迎查阅 <a href="https://github.com/WordPress/gutenberg/discussions/53022#discussioncomment-4696611">《为何选择 Preact 而非 Alpine?》</a> 专题讨论获取更多信息。
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
# 快速入门指南
|
||||
|
||||
本指南将帮助您构建一个基础区块,用于展示 WordPress 中的交互性 API 功能。
|
||||
|
||||
## 创建交互式区块
|
||||
|
||||
首先请确保您的计算机已安装 Node.js 和 `npm`。若尚未安装,请查阅 [Node.js 开发环境](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/)指南。
|
||||
|
||||
接下来,使用 [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) 包和 [`@wordpress/create-block-interactive-template`](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) 模板来搭建完整的“我的第一个交互式区块”插件。
|
||||
|
||||
选择要创建插件的目录,然后在该目录下的终端中执行以下命令:
|
||||
|
||||
```
|
||||
npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template
|
||||
```
|
||||
|
||||
提供的别名(`my-first-interactive-block`)将定义搭建插件的目录名称和内部区块名称。
|
||||
|
||||
## 基础使用
|
||||
|
||||
激活插件后,您可以探索该区块的工作方式。使用以下命令进入新创建的插件目录并启动开发流程:
|
||||
|
||||
```
|
||||
cd my-first-interactive-block && npm start
|
||||
```
|
||||
|
||||
当 `create-block` 搭建区块时,它会自动安装 `wp-scripts` 并将最常用的脚本添加到区块的 `package.json` 文件中。关于此包的详细介绍,请参阅 [wp-scripts 入门指南](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/)。
|
||||
|
||||
执行 `npm start` 命令将启动开发服务器,并监视区块代码的变更,在代码修改时自动重新构建区块。
|
||||
|
||||
完成修改后,运行 `npm run build` 命令。这将优化区块代码,使其达到生产环境标准。
|
||||
|
||||
## 查看区块效果
|
||||
|
||||
如果您已运行本地 WordPress 环境,可以在该环境的 `plugins` 目录中执行上述命令。若尚未搭建环境,可通过在插件目录(`my-first-interactive-block`)中执行以下命令,使用 [`@wp-playground/cli`](https://github.com/WordPress/wordpress-playground/tree/trunk/packages/playground/cli) 启动已安装该插件的 WordPress 站点:
|
||||
|
||||
```
|
||||
npx @wp-playground/cli server --auto-mount
|
||||
```
|
||||
|
||||
您将能够在任意文章中插入“我的第一个交互式区块”,并在发布后查看其在前端的表现效果。
|
||||
|
||||
<div class="callout callout-info">
|
||||
<p>如需获取交互式 API 的高级使用示例,可查阅以下资源:</p>
|
||||
<ul>
|
||||
<li><a href="https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/#docs-examples">文档与示例</a></li>
|
||||
<li><a href="https://github.com/WordPress/gutenberg/discussions/52894">入门指南与其他学习资源</a></li>
|
||||
<li><a href="https://github.com/WordPress/gutenberg/discussions/55642#">交互式 API 功能展示</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
Reference in New Issue
Block a user