## 经典主题中的指令处理
在交互区块中,只要在 `block.json` 文件中添加 `supports.interactivity`,服务器指令处理便会自动进行。但经典主题呢?
经典主题同样可以使用交互性 API,如果希望利用服务器指令处理(推荐这样做),可以通过 `wp_interactivity_process_directives` 函数实现。该函数接收包含未处理指令的 HTML 标记,并根据全局状态、局部上下文及派生状态的初始值返回修改后的 HTML 标记。
```php
// 初始化全局状态与派生状态...
wp_interactivity_state( '...', /* ... */ );
// 包含指令的交互式 HTML 标记
$html = '
...
';
// 处理指令,使其准备好发送至客户端
$processed_html = wp_interactivity_process_directives( $html );
```
就这样!无需其他操作。
若要在模板文件中使用 `wp_interactivity_process_directives`,可通过 `ob_start` 和 `ob_get_clean` 捕获 HTML 输出并在渲染前进行处理。
```php
...
添加芒果
```
这个新按钮具有指向 `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,为新增水果添加 `` 元素
```html
```
重要提示:当状态已在服务端初始化时,客户端无需重复初始化。
```javascript
store( 'myFruitPlugin', {
state: {
fruits: [ '苹果', '香蕉', '樱桃' ], // 此处无需重复定义!
},
} );
```
## 在服务端初始化派生状态
无论派生状态是源自全局状态、局部上下文还是两者结合,均可通过服务端指令处理在服务端进行预处理。
_建议阅读[理解全局状态、局部上下文与派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)指南,深入了解 Interactivity API 中派生状态的运作机制。_
### 可静态定义的派生状态
假设我们需要添加一个删除所有水果的按钮:
```html
删除所有水果
```
```javascript
const { state } = store( 'myFruitPlugin', {
actions: {
// ...
deleteFruits() {
state.fruits = [];
},
},
} );
```
接下来,我们希望在无水果时显示特定提示信息。通过使用引用派生状态 `state.hasFruits` 的 `data-wp-bind--hidden` 指令,可以控制提示信息的显示/隐藏:
```html
```
派生状态 `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
```
- 使用 `wp_interactivity_data_wp_context` 辅助函数。
```php
array( 'Apple', 'Banana', 'Cherry' ) );
?>
```
- **3. 使用指令定义交互式元素**
接下来,需要在 HTML 标记中添加必要的指令。
```html
```
在此示例中:
- `data-wp-interactive` 指令激活 DOM 元素及其子元素的交互性。
- `data-wp-each` 指令用于渲染元素列表。该指令可以在 `` 标签中使用,其值是指向存储在全局状态或本地上下文中的数组的引用路径。
- `data-wp-text` 指令设置 HTML 元素的内部文本。此处指向 `context.item`,这是 `data-wp-each` 指令存储数组每个项的位置。
使用本地上下文而非全局状态时,也可以使用完全相同的指令。唯一的区别是 `data-wp-each` 指向 `context.fruits` 而不是 `state.fruits`:
```html
```
就是这样!一旦你使用 `supports.interactivity` 设置了交互式区块,初始化了全局状态或本地上下文,并将指令添加到 HTML 标记中,Interactivity API 将处理其余的工作。开发人员无需编写额外的代码来在服务器端处理这些指令。
在幕后,WordPress 使用 `wp_interactivity_process_directives` 函数来查找和处理区块 HTML 标记中的指令。该函数使用 HTML API 根据找到的指令以及初始的全局状态和/或本地上下文对标记进行必要的更改。
因此,发送到浏览器的 HTML 标记已经是其最终形式,所有指令都已正确处理。这意味着当页面首次在浏览器中加载时,它已经包含了所有交互式元素的正确初始状态,无需任何 JavaScript 来修改它。
以下是水果列表示例的最终 HTML 标记的样子(指令已省略):
```html
```
如你所见,`data-wp-each` 指令为数组中的每个水果生成了一个 `` 元素,并且 `data-wp-text` 指令已被处理,用正确的水果名称填充了每个 ` `。
### 需要动态定义的派生状态
在大多数情况下,初始派生状态可以像前一个示例那样静态定义。但有时,该值依赖于服务器端也会变化的动态值,此时需要在 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
```
至此,客户端一切正常,访问者将看到购物清单中水果旁显示正确的表情符号。但由于 `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
array( __( '苹果' ), __( '香蕉' ), __( '樱桃' ) ),
'shoppingList' => array( __( '苹果' ), __( '樱桃' ) ),
// ...
?>
```
完成!由于交互性 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
```
从服务器序列化信息在其他场景中也很有用,例如传递 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 );
},
},
} );
```