title: "理解全局状态、局部上下文、派生状态与配置" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - Core Concepts - Interactivity Api - Reference Guides


理解全局状态、局部上下文、派生状态与配置

Interactivity API 为创建交互式区块提供了强大的框架。为了充分利用其功能,理解何时使用全局状态、局部上下文、派生状态或配置至关重要。本指南将阐明这些概念,并提供实用示例来帮助您决定何时使用每种方式。

Interactivity API 区分了响应式数据(变化时触发 UI 更新)和非响应式数据(在客户端生命周期内保持静态)。让我们从每个概念的简要定义开始。

响应式(状态与上下文):

非响应式:

现在让我们深入探讨这些概念,更详细地研究它们并提供一些示例。

全局状态

全局状态在交互性 API 中指的是页面上任何交互块都能访问和修改的全局数据。它充当共享信息中心,允许块的不同部分进行通信并保持同步。全局状态是交互块之间交换信息的理想机制,无论它们在 DOM 树中的位置如何。

以下情况应使用全局状态:

使用全局状态

```

  • 指令处理完成后准备发送到浏览器的 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--click="actions.toggle">Toggle</button>
    

    ```

  • 请访问服务器端渲染指南以了解更多关于指令在服务器端如何处理的信息。

    如果全局状态在 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' 命名空间的状态。 ```

  • You can also destructure the object returned by store:

    ```js
    const { state } = store( 'myPlugin' );
    ```
    
    And you can do the same even if you are defining the store at that moment, which is the most common scenario:
    
    ```js
    const { state } = store( 'myPlugin', {
        state: {
            // ...
        },
        actions: {
            toggle() {
                state.show = ! state.show;
            },
        },
    } );
    ```
    
    The global state initialized on the server using the `wp_interactivity_state` function is also included in that object because it is automatically serialized from the server to the client:
    
    ```php
    wp_interactivity_state( 'myPlugin', array(
      'someValue' => 1,
    ));
    ```
    
    ```js
    const { state } = store( 'myPlugin', {
        state: {
            otherValue: 2,
        },
        actions: {
            readGlobalState() {
                state.someValue; // It exists and its initial value is 1.
                state.otherValue; // It exists and its initial value is 2.
            },
        },
    } );
    ```
    
    Lastly, all calls to the `store` function with the same namespace are merged together:
    
    ```js
    store( 'myPlugin', { state: { someValue: 1 } } );
    
    store( 'myPlugin', { state: { otherValue: 2 } } );
    
    /* All calls to `store` return a stable reference to the same object, so you
     * can get a reference to `state` from any of them. */
    const { state } = store( 'myPlugin' );
    
    store( 'myPlugin', {
        actions: {
            readValues() {
                state.someValue; // It exists and its initial value is 1.
                state.otherValue; // It exists and its initial value is 2.
            },
        },
    } );
    ```
    

    示例:使用全局状态通信的两个交互式区块

    在此示例中,有两个独立的交互式区块。一个显示计数器,另一个显示用于递增该计数器的按钮。这些区块可以放置在页面上的任何位置,不受 HTML 结构限制。换句话说,一个区块无需是另一个区块的内部区块。

    在此示例中:

    1. 全局状态在服务器端使用 wp_interactivity_state 初始化,将 counter 的初始值设为 0。
    2. 计数器区块使用 data-wp-text="state.counter" 显示当前计数器值,该指令从全局状态读取数据。
    3. 递增区块包含一个按钮,点击时使用 data-wp-on--click="actions.increment" 触发 increment 操作。
    4. 在 JavaScript 中,increment 操作通过递增 state.counter 直接修改全局状态。

    两个区块相互独立,可以放置在页面上的任何位置。它们无需在 DOM 结构中嵌套或直接关联。可以在页面上添加这些交互式区块的多个实例,它们都将共享并更新同一个全局计数器值。

    局部上下文

    Interactivity API 中的局部上下文指的是在 HTML 结构中特定元素内定义的局部数据。与全局状态不同,局部上下文仅对定义它的元素及其子元素可访问。

    当您需要为独立的交互块维护独立状态时,局部上下文特别有用,它能确保每个块实例可以维护自己独特的数据,而不会干扰其他实例。

    在以下情况下应使用局部上下文:

    Working with local context

    主题:

    计数器:

    </div>
    ```
    
    在此示例中,内部的 `div` 将拥有 `"dark"` 的 `theme` 值,但会从其父级上下文继承 `counter` 值 `0`。
    

    示例:使用本地上下文实现独立状态的交互块

    在此示例中,单个交互块显示一个计数器并可以递增。通过使用本地上下文,即使页面上添加了多个此类块,每个实例都将拥有自己独立的计数器。

    <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--click="actions.increment">递增</button>
    </div>
    
    store( 'myCounterPlugin', {
        actions: {
            increment() {
                const context = getContext();
                context.counter += 1;
            },
        },
    } );
    

    在此示例中:

    1. 使用 data-wp-context 指令定义了一个初始 counter 值为 0 的本地上下文。
    2. 使用 data-wp-text="context.counter" 显示计数器,该指令从本地上下文读取值。
    3. 递增按钮使用 data-wp-on--click="actions.increment" 来触发递增操作。
    4. 在 JavaScript 中,使用 getContext 函数访问和修改每个块实例的本地上下文。

    用户可以在页面上添加此块的多个实例,每个实例都将维护自己独立的计数器。点击一个块上的“递增”按钮只会影响该特定块的计数器,而不会影响其他块。

    派生状态

    派生状态在交互性 API 中指的是从全局状态或本地上下文的其他部分计算得出的值。它是按需计算的,而非存储的。这确保了数据一致性,减少了冗余,并增强了代码的声明性。

    派生状态是现代状态管理中的一个基本概念,并非交互性 API 所独有。它也被用于其他流行的状态管理系统,例如在 Redux 中被称为 selectors,在 Preact Signals 中被称为 computed 值。

    派生状态提供了几个关键优势,使其成为设计良好的应用程序状态的重要组成部分,包括:

    1. 单一数据源: 派生状态鼓励您只在状态中存储必要的基础数据。任何可以从这些核心数据计算得出的值都成为派生状态。这种方法降低了交互块中出现数据不一致的风险。

    2. 自动更新: 当您使用派生状态时,只要底层数据发生变化,值就会自动重新计算。这确保了交互块的所有部分始终能访问到最新的信息,无需手动干预。

    3. 简化状态管理: 通过按需计算值,而不是手动存储和更新它们,您可以降低状态管理逻辑的复杂性。这带来了更清晰、更易于维护的代码。

    4. 提升性能: 在许多情况下,派生状态可以被优化为仅在必要时重新计算,这可能会提升交互块的性能。

    5. 便于调试: 使用派生状态,数据的来源和转换方式更加清晰。这可以使追踪交互块中的问题变得更容易。

    本质上,派生状态允许您以声明式的方式表达交互块中不同数据片段之间的关系,而不是在每次发生变化时命令式地更新相关值。

    请访问反应式和声明式思维指南,了解更多关于如何在交互性 API 中利用声明式编码的信息。

    您应该在以下情况下使用派生状态:

    Working with derived state

        <?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>
        ```
    
        This `data-wp-each` template will render this HTML (directives omitted):
    
        ```html
        <span>3</span>
        <span>6</span>
        <span>9</span>
        ```
    
    -   **Accessing the derived state**
    
        In the HTML markup, the syntax for the derived state is the same as the one for the global state, just by referencing `state` in the directive attribute values.
    
        ```html
        <span data-wp-text="state.double"></span>
        ```
    
        The same happens in JavaScript. Both global state and derived state can be consumed through the `state` property of the store:
    
        ```js
        const { state } = store( 'myCounterPlugin', {
            // ...
            actions: {
                readValues() {
                    state.counter; // Regular state, returns 1.
                    state.double; // Derived state, returns 2.
                },
            },
        } );
        ```
    
        This lack of distinction is intentional, allowing developers to consume both derived and global state uniformly, and making them interchangeable in practice.
    
        You can also access the derived state from another derived state and, thus, create multiple levels of computed values.
    
        ```js
        const { state } = store( 'myPlugin', {
            state: {
                get double() {
                    return state.counter * 2;
                },
                get doublePlusOne() {
                    return state.double + 1;
                },
            },
        } );
        ```
    
    -   **Updating the derived state**
    
        The derived state cannot be updated directly. To update its values, you need to update the global state or local context on which that derived state depends.
    
        ```js
        const { state } = store( 'myCounterPlugin', {
            // ...
            actions: {
                updateValues() {
                    state.counter; // Regular state, returns 1.
                    state.double; // Derived state, returns 2.
    
                    state.counter = 2;
    
                    state.counter; // Regular state, returns 2.
                    state.double; // Derived state, returns 4.
                },
            },
        } );
        ```
    
    ### 示例:不使用派生状态 vs 使用派生状态
    
    让我们考虑一个场景:有一个计数器需要显示其双倍值,并比较两种方法:一种不使用派生状态,另一种使用派生状态。
    
    -   **不使用派生状态**
    
        ```js
        const { state } = store( 'myCounterPlugin', {
            state: {
                counter: 1,
                double: 2,
            },
            actions: {
                increment() {
                    state.counter += 1;
                    state.double = state.counter * 2;
                },
            },
        } );
        ```
    
        在这种方法中,`state.counter` 和 `state.double` 的值都在 `increment` 操作中手动更新。虽然这可行,但有几个缺点:
    
        -   声明性较差。
        -   如果 `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` 被定义为一个 getter,自动从 `state.counter` 派生其值。
        -   `increment` 操作只需要更新 `state.counter`。
        -   无论 `state.counter` 如何或在何处更新,`state.double` 都始终保证具有正确的值。
    
    ### Example: Using derived state with local context
    
    Let's now consider a scenario where there is a local context that initializes a counter.
    
    ```js
    store( 'myCounterPlugin', {
        state: {
            get double() {
                const { counter } = getContext();
                return counter * 2;
            },
        },
        actions: {
            increment() {
                const context = getContext();
                context.counter += 1;
            },
        },
    } );
    
    <div data-wp-interactive="myCounterPlugin">
        <!-- This will render "Double: 2" -->
        <div data-wp-context='{ "counter": 1 }'>
            Double: <span data-wp-text="state.double"></span>
    
            <!-- This button will increment the local counter. -->
            <button data-wp-on--click="actions.increment">Increment</button>
        </div>
    
        <!-- This will render "Double: 4" -->
        <div data-wp-context='{ "counter": 2 }'>
            Double: <span data-wp-text="state.double"></span>
    
            <!-- This button will increment the local counter. -->
            <button data-wp-on--click="actions.increment">Increment</button>
        </div>
    </div>
    

    In this example, the derived state state.double reads from the local context present in each element and returns the correct value for each instance where it is used.

    Example: Using derived state with both local context and global state

    Let's now consider a scenario where there is a global tax rate and local product prices and calculate the final price, including tax.

    <div
        data-wp-interactive="myProductPlugin"
        data-wp-context='{ "priceWithoutTax": 100 }'
    >
        <p>Product Price: $<span data-wp-text="context.priceWithoutTax"></span></p>
        <p>Tax Rate: <span data-wp-text="state.taxRatePercentage"></span></p>
        <p>Price (inc. tax): $<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 ) {
                // Updates the global tax rate.
                state.taxRate = event.target.value;
            },
            updatePrice( event ) {
                // Updates the local product price.
                const context = getContext();
                context.priceWithoutTax = event.target.value;
            },
        },
    } );
    

    In this example, priceWithTax is derived from both the global taxRate and the local priceWithoutTax. Every time you update the global state or local context through the updateTaxRate or updatePrice actions, the Interactivity API recomputes the derived state and updates the necessary parts of the DOM.

    By using derived state, you create a more maintainable and less error-prone codebase. It ensures that related state values are always in sync, reduces the complexity of your actions, and makes your code more declarative and easier to reason about.

    订阅服务器状态与上下文

    交互性 API 提供了一种基于区域导航的功能,能够动态替换页面部分内容而无需整页重新加载。当禁用“强制页面重新加载”开关时,查询区块原生支持此功能。开发者可通过调用 @wordpress/interactivity-router 脚本模块中的 actions.navigate() 在自定义区块中实现相同功能。

    请访问客户端导航指南了解如何使用交互路由器和在区块中实现客户端导航。

    使用基于区域导航时,必须确保交互式区块与服务器提供的全局状态和本地上下文保持同步。默认情况下,交互性 API 不会用服务器提供的值覆盖全局状态或本地上下文。该 API 提供了两个函数来帮助管理同步:getServerState()getServerContext()

    getServerState()

    getServerState() allows you to subscribe to changes in the global state that occur during client-side navigation. This function is analogous to getServerContext(), but it works with the global state instead of the local context.

    The getServerState() function returns a read-only reactive object. This means that any callbacks you have defined that watch the returned object will only trigger when the value returned by the function changes. If the value remains the same, the callback will not re-trigger.

    Let's consider a quiz that has multiple questions. Each question is a separate page. When the user navigates to a new question, the server provides the new question and the time left to answer all the questions.

    <?php
    wp_interactivity_state( 'myPlugin', array(
        'question' => get_question_for_page( get_the_ID() ),
        'timeLeft' => 5 * 60, // Time to answer all the questions.
    ) );
    ?>
    <div data-wp-interactive="myPlugin">
    
    import { store, getServerState, withSyncEvent } from '@wordpress/interactivity';
    
    const { state } = store( 'myPlugin', {
        actions: {
            // This action would be triggered by a directive, like:
            // <button data-wp-on--click="actions.nextQuestion">Next Question</button>
            nextQuestion: withSyncEvent( function* ( event ) {
                event.preventDefault();
                const { actions } = yield import(
                    '@wordpress/interactivity-router'
                );
                actions.navigate( '/question-2' );
            } ),
        },
        callbacks: {
            // This callback would be triggered by a directive, like:
            // <div data-wp-watch="callbacks.updateQuestion"></div>
            updateQuestion() {
                const serverState = getServerState();
    
                // Update with the new value coming from the server.
                // We DON'T want to update `timeLeft` because it represents the time left to answer ALL the questions.
                state.question = serverState.question;
            },
        },
    } );
    

    Note: Actions that need to call synchronous event methods like event.preventDefault() must wrap the handler with withSyncEvent(). See the withSyncEvent() documentation for details.

    getServerContext()

    getServerContext() allows you to subscribe to changes in the local context that occur during client-side navigation. This function is analogous to getServerState(), but it works with the local context instead of the global state.

    The getServerContext() function returns a read-only reactive object. This means that any callbacks you have defined that watch the returned object will only trigger when the value returned by the function changes. If the value remains the same, the callback will not re-trigger.

    Consider a quiz that has multiple questions. Each question is a separate page. When the user navigates to a new question, the server provides the new question and the time left to answer all the questions.

    <div <?php echo wp_interactivity_data_wp_context( array(
        'currentQuestion' => get_question_for_page( get_the_ID() ),
    ), ); ?>>
    
    import {
        store,
        getContext,
        getServerContext,
        withSyncEvent,
    } from '@wordpress/interactivity';
    
    store( 'myPlugin', {
        actions: {
            // This action would be triggered by a directive, like:
            // <button data-wp-on--click="actions.nextQuestion">Next Question</button>
            nextQuestion: withSyncEvent( function* ( event ) {
                event.preventDefault();
                const { actions } = yield import(
                    '@wordpress/interactivity-router'
                );
                actions.navigate( '/question-2' );
            } ),
        },
        callbacks: {
            // This callback would be triggered by a directive, like:
            // <div data-wp-watch="callbacks.updateQuestion"></div>
            updateQuestion() {
                const serverContext = getServerContext();
                const context = getContext();
    
                // Update with the new value coming from the server.
                context.currentQuestion = serverContext.currentQuestion;
            },
        },
    } );
    

    何时使用

    当您拥有依赖全局状态或本地上下文的交互式区块,且这些状态可能因导航事件而改变时,确保应用程序不同部分的一致性。

    使用 getServerState()getServerContext() 的最佳实践

    配置

    配置在交互性 API 中指的是从服务器序列化到客户端的静态配置数据。与全局状态或本地上下文不同,配置值是非响应式的——它们不会触发 UI 更新,并在整个客户端生命周期中保持不变。

    配置非常适合从 PHP 向 JavaScript 发送非响应式数据,例如 API 端点、随机数、功能开关或在用户交互期间不会改变的翻译。

    在以下情况下应使用配置:

    配置使用指南

    结论

    请记住,有效状态管理的关键在于保持状态最小化并避免冗余。使用派生状态动态计算值,根据数据的作用域和需求在全局状态与本地上下文之间做出选择,并使用配置处理静态的服务器到客户端数据。这将带来更清晰、更健壮的架构,更易于调试和维护。最后,如果需要将状态或上下文与服务器同步,可以使用 getServerState()getServerContext() 来实现。

    ← 返回文档中心