28 KiB
使用时机
当存在依赖全局状态或本地上下文的交互模块,且这些内容可能因导航事件而改变时,使用此功能可确保应用程序不同部分的一致性。
getServerState() 与 getServerContext() 使用最佳实践
- 只读引用:
getServerState()和getServerContext()均返回只读对象。您可以使用这些对象来更新全局状态或本地上下文。 - 回调集成: 在存储回调函数中集成这些函数,以响应状态和上下文的变化。
getServerState()和getServerContext()返回的都是响应式对象,这意味着它们的监听回调仅在属性值发生变化时触发。若属性值保持不变,则不会重新触发回调。
总结
请记住,有效状态管理的关键在于保持状态最小化并避免冗余。使用派生状态动态计算值,根据数据的作用域和需求在全局状态与本地上下文之间做出选择。这将有助于构建更清晰、更健壮的架构,使调试和维护更为轻松。最后,若需将状态或上下文与服务器同步,可通过 getServerState() 和 getServerContext() 实现这一目标。
示例:使用局部上下文实现独立状态的交互区块
此示例展示了一个交互区块,它显示计数器并支持递增操作。通过使用局部上下文,该区块的每个实例都将拥有独立的计数器,即使页面中添加了多个此类区块。
<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>
store( 'myCounterPlugin', {
actions: {
increment() {
const context = getContext();
context.counter += 1;
},
},
} );
本示例说明:
- 通过
data-wp-context指令定义初始值为0的局部计数器上下文 - 使用
data-wp-text="context.counter"从局部上下文读取并显示计数器值 - 递增按钮通过
data-wp-on-async--click="actions.increment"触发递增操作 - JavaScript 中的
getContext函数用于访问和修改每个区块实例的局部上下文
用户可在页面中添加多个该区块实例,每个实例都将保持独立的计数器状态。点击某个区块的"递增"按钮仅影响该特定区块的计数器,不会对其他区块产生作用。
派生状态
Interactivity API 中的派生状态指根据全局状态或局部上下文的其他部分计算得出的值。该值按需计算而非直接存储,可确保一致性、减少冗余,并增强代码的声明性特性。
派生状态是现代状态管理的基础概念,并非 Interactivity API 独有。在其他主流状态管理系统中同样存在,例如 Redux 中称为“选择器”(selectors),Preact Signals 中称作“计算值”(computed values)。
派生状态具有以下核心优势,使其成为精心设计的应用状态中不可或缺的组成部分:
-
单一数据源:派生状态鼓励仅存储必要的原始数据,所有可基于核心数据计算的值都作为派生状态。这种方法可降低交互区块中出现数据不一致的风险。
-
自动更新:使用派生状态时,当基础数据发生变化,相关值会自动重新计算。这能确保交互区块的所有部分始终获取最新信息,无需人工干预。
-
简化状态管理:通过按需计算而非手动存储更新数值,可降低状态管理逻辑的复杂度,使代码更清晰、更易维护。
-
提升性能:多数情况下,派生状态可优化为仅在必要时重新计算,从而提升交互区块的性能表现。
-
便于调试:派生状态能更清晰地展现数据来源与转换过程,有助于快速定位交互区块中的问题。
本质上,派生状态允许以声明式方式表达交互区块中不同数据间的关联,而非在数据变化时强制更新相关值。
请访问响应式与声明式思维指南,深入了解如何在 Interactivity API 中运用声明式编码。
在以下场景中建议使用派生状态:
- 当部分全局状态或局部上下文可从其他状态值计算得出时
- 需要避免手动保持同步的冗余数据时
- 通过自动更新派生值确保交互区块间的一致性时
- 通过消除更新多个关联状态属性的需求来简化操作时
订阅服务端状态与上下文
交互性API提供基于区域的路由功能,能够动态替换页面局部内容而无需整页刷新。当禁用"强制页面重载"开关时,查询区块原生支持此功能。开发者可通过调用 @wordpress/interactivity-router 脚本模块中的 actions.navigate() 在自定义区块中实现相同功能。
使用基于区域的路由时,必须确保交互区块与服务端提供的全局状态和本地上下文保持同步。默认情况下,交互性API不会用服务端传值覆盖全局状态或本地上下文。该API提供两个函数来协助管理同步:getServerState() 和 getServerContext()。
getServerState()
getServerState() 用于订阅全局状态在客户端导航期间发生的变化。此函数与 getServerContext() 类似,但作用于全局状态而非本地上下文。
getServerState() 返回一个只读的响应式对象。这意味着任何监视该返回对象的回调函数仅会在函数返回值变更时触发。若值保持不变,回调不会重复触发。
以多题目测验为例:每个题目位于独立页面,当用户导航至新题目时,服务端将提供新题目及剩余答题时间。
<div <?php echo wp_interactivity_state( 'myPlugin', array(
'question' => get_question_for_page( get_the_ID() ),
'timeLeft' => 5 * 60, // 回答所有题目的总剩余时间
) ); ?>>
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() 返回一个只读的响应式对象。这意味着任何监视该返回对象的回调函数仅会在函数返回值变更时触发。若值保持不变,回调不会重复触发。
以多题目测验为例:每个题目位于独立页面,当用户导航至新题目时,服务端将提供新题目及剩余答题时间。
<div <?php echo wp_interactivity_data_wp_context( array(
'currentQuestion' => get_question_for_page( get_the_ID() ),
), ); ?>>
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 使用派生状态
考虑一个需要显示计数器及其两倍数值的场景,我们对比两种实现方式:未使用派生状态与使用派生状态。
-
未使用派生状态
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可能导致错误 - 需要额外关注相关值的更新,增加认知负担
-
使用派生状态
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总能保持正确值
示例:在本地上下文中使用派生状态
考虑一个初始化计数器的本地上下文场景:
store( 'myCounterPlugin', {
state: {
get double() {
const { counter } = getContext();
return counter * 2;
},
},
actions: {
increment() {
const context = getContext();
context.counter += 1;
},
},
} );
<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 从每个元素所在的本地上下文读取数据,为每个使用实例返回正确的值。
示例:同时使用本地上下文和全局状态的派生状态
考虑一个包含全局税率和本地商品价格,并计算含税最终价格的场景:
<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>
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函数在服务器上进行初始化,其方式与全局状态完全相同。-
当初始值已知且为静态时,可以直接定义:
wp_interactivity_state( 'myCounterPlugin', array( 'counter' => 1, // 这是全局状态。 'double' => 2, // 这是派生状态。 )); -
或者可以通过进行必要的计算来定义:
$counter = 1; $double = $counter * 2; wp_interactivity_state( 'myCounterPlugin', array( 'counter' => $counter, // 这是全局状态。 'double' => $double, // 这是派生状态。 ));
无论采用哪种方法,初始的派生状态值将在 PHP 渲染页面时使用,并且 HTML 将填充正确的值。
请访问 服务端渲染指南 以了解更多关于指令在服务器上如何处理的信息。
即使派生状态属性依赖于本地上下文,同样的机制也适用。
<?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 定义:
const { state } = store( 'myCounterPlugin', { state: { get double() { return state.counter * 2; }, }, } );派生状态可以依赖于本地上下文,或者同时依赖于本地上下文和全局状态。
const { state } = store( 'myCounterPlugin', { state: { get double() { const { counter } = getContext(); // 依赖于本地上下文。 return counter * 2; }, get product() { const { counter } = getContext(); // 依赖于本地上下文和全局状态。 return counter * state.factor; }, }, } );在某些情况下,当派生状态依赖于本地上下文,并且本地上下文在服务器上可以动态更改时,可以使用函数(闭包)动态计算派生状态,而不是使用初始派生状态。
<?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(指令省略):<span>3</span> <span>6</span> <span>9</span> -
-
访问派生状态
在 HTML 标记中,派生状态的语法与全局状态的语法相同,只需在指令属性值中引用
state。<span data-wp-text="state.double"></span>在 JavaScript 中也是如此。全局状态和派生状态都可以通过 store 的
state属性使用:const { state } = store( 'myCounterPlugin', { // ... actions: { readValues() { state.counter; // 常规状态,返回 1。 state.double; // 派生状态,返回 2。 }, }, } );这种无区别的设计是有意的,允许开发者统一使用派生状态和全局状态,并使它们在实践中可以互换。
你还可以从另一个派生状态访问派生状态,从而创建多级计算值。
const { state } = store( 'myPlugin', { state: { get double() { return state.counter * 2; }, get doublePlusOne() { return state.double + 1; }, }, } ); -
更新派生状态
派生状态不能直接更新。要更新其值,你需要更新该派生状态所依赖的全局状态或本地上下文。
const { state } = store( 'myCounterPlugin', { // ... actions: { updateValues() { state.counter; // 常规状态,返回 1。 state.double; // 派生状态,返回 2。 state.counter = 2; state.counter; // 常规状态,返回 2。 state.double; // 派生状态,返回 4。 }, }, } );
示例:使用全局状态通信的两个交互块
本示例包含两个独立的交互块:一个用于显示计数器,另一个包含递增计数器的按钮。这些块可以放置在页面任意位置,无需遵循特定的HTML结构。也就是说,一个块不必作为另一个块的内嵌块存在。
-
计数器块
<?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> -
递增块
<div data-wp-interactive="myCounterPlugin" <?php echo get_block_wrapper_attributes(); ?> > <button data-wp-on-async--click="actions.increment"> 递增 </button> </div>const { state } = store( 'myCounterPlugin', { actions: { increment() { state.counter += 1; }, }, } );
本示例说明:
- 通过服务端的
wp_interactivity_state初始化全局状态,设置计数器初始值为0 - 计数器块使用
data-wp-text="state.counter"读取全局状态并显示当前计数值 - 递增块包含的按钮通过
data-wp-on-async--click="actions.increment"在点击时触发递增操作 - JavaScript中的
increment操作通过递增state.counter直接修改全局状态
这两个块相互独立,可置于页面任意位置,无需在DOM结构中形成嵌套或直接关联。页面中可以添加多个此类交互块实例,它们将共享并更新同一全局计数值。
本地上下文
Interactivity API中的本地上下文特指在HTML结构特定元素内定义的局部数据。与全局状态不同,本地上下文仅可在定义元素及其子元素中访问。
本地上下文在以下场景中特别有用:
- 需要为多个交互块实例维护独立状态
- 需要封装仅与特定交互块及其子元素相关的数据
- 需要实现仅限于界面特定区域的隔离功能
本地上下文操作指南
-
初始化本地上下文
通过
data-wp-context指令直接在HTML结构中初始化本地上下文,该指令接收定义上下文初始值的JSON字符串:<div data-wp-context='{ "counter": 0 }'> <!-- 子元素可访问 `context.counter` --> </div>也可使用
wp_interactivity_data_wp_contextPHP辅助函数在服务端初始化,确保字符串化值的正确转义和格式化:<?php $context = array( 'counter' => 0 ); ?> <div <?php echo wp_interactivity_data_wp_context( $context ); ?>> <!-- 子元素可访问 `context.counter` --> </div> -
访问本地上下文
在HTML标记中,可通过指令值直接引用
context访问本地上下文值:<div data-wp-bind--hidden="!context.isOpen"> <span data-wp-text="context.counter"></span> </div>在JavaScript中,可使用
getContext函数访问本地上下文值:store( 'myPlugin', { actions: { sendAnalyticsEvent() { const { counter } = getContext(); myAnalyticsLibrary.sendEvent( 'updated counter', counter ); }, }, callbacks: { logCounter() { const { counter } = getContext(); console.log( `当前计数值:${ counter }` ); }, }, } );getContext函数返回触发操作/回调执行的元素对应的本地上下文。 -
更新本地上下文
在JavaScript中可通过修改
getContext返回的对象来更新本地上下文值:store( 'myPlugin', { actions: { increment() { const context = getContext(); context.counter += 1; }, updateName( event ) { const context = getContext(); context.name = event.target.value; }, }, } );对本地上下文的修改将自动触发依赖该值的所有指令更新。
请参阅响应式与声明式思维指南,深入了解Interactivity API中的响应式工作原理。
-
嵌套本地上下文
本地上下文支持嵌套结构,子上下文可继承并覆盖父上下文的值:
<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函数在服务端定义初始全局状态值:// 填充初始全局状态值 wp_interactivity_state( 'myPlugin', array( 'isDarkTheme' => true, 'show' => false, 'helloText' => __( 'world' ), ));这些初始全局状态值将在 PHP 渲染页面时用于填充发送至浏览器的 HTML 标记。
-
开发者在 PHP 文件中编写的 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 标记:
<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>
请访问服务端渲染指南了解指令在服务端处理的详细信息。
若全局状态未在 PHP 页面渲染过程中使用,也可直接在客户端定义:
const { state } = store( 'myPlugin', { state: { isLoading: false, }, actions: { *loadSomething() { state.isLoading = true; // ... }, }, } );请注意,虽然这种方式可行,但通常建议在服务端定义所有全局状态。
-
-
访问全局状态
在 HTML 标记中,可通过在指令属性值中引用
state直接访问全局状态值:<div data-wp-bind--hidden="!state.show"> <span data-wp-text="state.helloText"></span> </div>在 JavaScript 中,
@wordpress/interactivity包提供的store函数兼具设置器和获取器功能,返回指定命名空间的存储对象。要在操作和回调中访问全局状态,可使用
store函数返回对象的state属性:const myPluginStore = store( 'myPlugin' ); myPluginStore.state; // 这是 'myPlugin' 命名空间的状态也可对
store返回的对象进行解构:const { state } = store( 'myPlugin' );即使在定义存储时也可采用相同方式,这是最常见的使用场景:
const { state } = store( 'myPlugin', { state: { // ... }, actions: { toggle() { state.show = ! state.show; }, }, } );使用
wp_interactivity_state函数在服务端初始化的全局状态也会自动包含在该对象中,因为它会从服务端自动序列化到客户端:wp_interactivity_state( 'myPlugin', array( 'someValue' => 1, ));const { state } = store( 'myPlugin', { state: { otherValue: 2, }, actions: { readGlobalState() { state.someValue; // 存在且初始值为 1 state.otherValue; // 存在且初始值为 2 }, }, } );最后,所有对同一命名空间的
store函数调用都会合并: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对象后对其进行修改:const { state } = store( 'myPlugin', { actions: { updateValues() { state.someValue = 3; state.otherValue = 4; }, }, } );对全局状态的更改将自动触发依赖修改值的所有指令更新。
请访问响应式与声明式思维指南了解 Interactivity API 中响应式工作原理的详细信息。