title: "指令与状态存储" post_status: publish comment_status: open taxonomy: category: - gutenberg-docs post_tag: - Interactivity Api - Reference Guides - Repos


指令与状态存储

交互性 API 仅适用于 WordPress 6.5 及以上版本。

开发者可通过以下方式为区块添加交互功能:

DOM 元素通过指令与存储在状态和上下文中的数据建立连接。当状态或上下文中的数据发生变化时,指令将响应这些变化并相应更新 DOM(参见示意图)。

状态与指令

什么是指令?

指令是添加到区块标记中的自定义属性,用于为其 DOM 元素添加行为。这可以在 render.php 文件(针对动态区块)或 save.js 文件(针对静态区块)中完成。

交互性 API 指令使用 data- 前缀。以下是在 HTML 标记中使用指令的示例。

<div
    data-wp-interactive="myPlugin"
    data-wp-context='{ "isOpen": false }'
    data-wp-watch="callbacks.logIsOpen"
>
    <button
        data-wp-on--click="actions.toggle"
        data-wp-bind--aria-expanded="context.isOpen"
        aria-controls="p-1"
    >
        Toggle
    </button>

    <p id="p-1" data-wp-bind--hidden="!context.isOpen">
        This element is now visible!
    </p>
</div>

指令也可以使用 HTML 标签处理器 动态注入。

通过指令,您可以直接管理交互,例如副作用、状态、事件处理器、属性或内容。

List of Directives

wp-interactive

The wp-interactive directive "activates" the interactivity for the DOM element and its children through the Interactivity API (directives and store). The directive includes a namespace to reference a specific store, that can be set as a string or an object.

<!-- Let's make this element and its children interactive and set the namespace -->
<div
    data-wp-interactive="myPlugin"
    data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
    <p>
        I'm interactive now,
        <span data-wp-style--background-color="context.myBgColor"
            >and I can use directives!</span
        >
    </p>
    <div>
        <p>
            I'm also interactive,
            <span data-wp-style--color="context.myColor"
                >and I can also use directives!</span
            >
        </p>
    </div>
</div>

<!-- This is also valid -->
<div
    data-wp-interactive='{ "namespace": "myPlugin" }'
    data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }'
>
    <p>
        I'm interactive now,
        <span data-wp-style--background-color="context.myBgColor"
            >and I can use directives!</span
        >
    </p>
    <div>
        <p>
            I'm also interactive,
            <span data-wp-style--color="context.myColor"
                >and I can also use directives!</span
            >
        </p>
    </div>
</div>
The use of data-wp-interactive is a requirement for the Interactivity API "engine" to work. In the following examples the data-wp-interactive has not been added for the sake of simplicity. Also, the data-wp-interactive directive will be injected automatically in the future.

wp-context

It provides a local state available to a specific HTML node and its children.

The wp-context directive accepts a stringified JSON as a value.

// render.php
<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }' >
  <button data-wp-on--click="actions.logId" >
    Click Me!
  </button>
</div>
See store used with the directive above
store( 'myPlugin', {
    actions: {
        logId: () => {
            const { post } = getContext();
            console.log( post.id );
        },
    },
} );

Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one:

<div data-wp-context='{ "foo": "bar" }'>
    <span data-wp-text="context.foo"><!-- Will output: "bar" --></span>

    <div data-wp-context='{ "bar": "baz" }'>
        <span data-wp-text="context.foo"><!-- Will output: "bar" --></span>

        <div data-wp-context='{ "foo": "bob" }'>
            <span data-wp-text="context.foo"><!-- Will output: "bob" --></span>
        </div>
    </div>
</div>

wp-bind

This directive allows setting HTML attributes on elements based on a boolean or string value. It follows the syntax data-wp-bind--attribute.

<li data-wp-context='{ "isMenuOpen": false }'>
    <button
        data-wp-on--click="actions.toggleMenu"
        data-wp-bind--aria-expanded="context.isMenuOpen"
    >
        Toggle
    </button>
    <div data-wp-bind--hidden="!context.isMenuOpen">
        <span>Title</span>
        <ul>
            SUBMENU ITEMS
        </ul>
    </div>
</li>
See store used with the directive above
store( 'myPlugin', {
    actions: {
        toggleMenu: () => {
            const context = getContext();
            context.isMenuOpen = ! context.isMenuOpen;
        },
    },
} );

The wp-bind directive is executed:

When wp-bind directive references a callback to get its final value:

The wp-bind will do different things when the DOM element is applied, depending on its value:

wp-class

This directive adds or removes a class to an HTML element, depending on a boolean value. It follows the syntax data-wp-class--classname.

<div>
    <li
        data-wp-context='{ "isSelected": false }'
        data-wp-on--click="actions.toggleSelection"
        data-wp-class--selected="context.isSelected"
    >
        Option 1
    </li>
    <li
        data-wp-context='{ "isSelected": false }'
        data-wp-on--click="actions.toggleSelection"
        data-wp-class--selected="context.isSelected"
    >
        Option 2
    </li>
</div>
See store used with the directive above
store( 'myPlugin', {
    actions: {
        toggleSelection: () => {
            const context = getContext();
            context.isSelected = ! context.isSelected;
        },
    },
} );

The wp-class directive is executed:

The boolean value received by the directive is used to toggle (add when true or remove when false) the associated class name from the class attribute.

It's important to note that when using the wp-class directive, it's recommended to use kebab-case for class names instead of camelCase. This is because HTML attributes are not case-sensitive, and HTML will treat data-wp-class--isDark the same as data-wp-class--isdark or DATA-WP-CLASS--ISDARK.

So, for example, use the class name is-dark instead of isDark and data-wp-class--is-dark instead of data-wp-class--isDark:

<!-- Recommended -->
<div data-wp-class--is-dark="context.isDarkMode">
    <!-- ... -->
</div>

<!-- Not recommended -->
<div data-wp-class--isDark="context.isDarkMode">
    <!-- ... -->
</div>
/* Recommended */
.is-dark {
    /* ... */
}

/* Not recommended */
.isDark {
    /* ... */
}

wp-style

该指令根据其值向 HTML 元素添加或移除内联样式。它遵循语法 data-wp-style--css-property

<div data-wp-context='{ "color": "red" }'>
    <button data-wp-on--click="actions.toggleContextColor">
        Toggle Color Text
    </button>
    <p data-wp-style--color="context.color">Hello World!</p>
</div>
查看与上述指令配合使用的 store
store( 'myPlugin', {
    actions: {
        toggleContextColor: () => {
            const context = getContext();
            context.color = context.color === 'red' ? 'blue' : 'red';
        },
    },
} );

wp-style 指令在以下情况下执行:

指令接收到的值用于添加或移除具有关联 CSS 属性的 style 属性:

wp-text

该指令用于设置 HTML 元素的内部文本。

<div data-wp-context='{ "text": "Text 1" }'>
    <span data-wp-text="context.text"></span>
    <button data-wp-on--click="actions.toggleContextText">
        Toggle Context Text
    </button>
</div>
查看与上述指令配合使用的 store
store( 'myPlugin', {
    actions: {
        toggleContextText: () => {
            const context = getContext();
            context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1';
        },
    },
} );

wp-text 指令在以下情况下执行:

返回值用于更改元素的内部内容:<div>value</div>

wp-on

此指令默认异步运行以提高性能。如果你需要同步访问 event 对象,请使用 withSyncEvent() 包装你的操作。对于也需要同步事件访问的异步操作,你可以将 withSyncEvent() 与一个 生成器函数 结合使用,该函数在调用同步 API 后让出主线程。

此指令在派发的 DOM 事件(如 clickkeyup)上运行代码。语法为 data-wp-on--[event](例如 data-wp-on--clickdata-wp-on--keyup)。

<button data-wp-on--click="actions.logTime" >
  Click Me!
</button>
查看与上述指令一起使用的 store
store( 'myPlugin', {
    actions: {
        logTime: ( event ) => {
            console.log( new Date() );
        },
    },
} );

每次触发关联事件时,都会执行 wp-on 指令。

作为引用传递的回调接收 事件 (event),并且此回调的返回值会被忽略。

wp-on-window

默认情况下,此指令以异步方式运行以提升性能。如需同步访问 event 对象,请使用 withSyncEvent() 包装你的操作。对于同时需要同步事件访问的异步操作,你可以将 withSyncEvent() 与一个 生成器函数 结合使用,该函数在调用同步 API 后让出主线程。

此指令允许你附加全局窗口事件,如 resizecopyfocus,并在这些事件发生时执行定义的回调函数。

支持的窗口事件列表。

该指令的语法为 data-wp-on-window--[window-event](例如 data-wp-on-window--resizedata-wp-on-window--languagechange)。

<div data-wp-on-window--resize="callbacks.logWidth"></div>
查看与上述指令配合使用的 store
store( 'myPlugin', {
    callbacks: {
        logWidth() {
            console.log( 'Window width: ', window.innerWidth );
        },
    },
} );

作为引用传递的回调函数接收 事件对象 (event),并且此回调的返回值将被忽略。当元素从 DOM 中移除时,事件监听器也会被移除。

wp-on-document

默认情况下,此指令以异步方式运行以提高性能。如果您需要同步访问 event 对象,请使用 withSyncEvent() 包装您的操作。对于也需要同步事件访问的异步操作,您可以结合使用 withSyncEvent() 和一个 生成器函数,该函数在调用同步 API 后让出主线程。

此指令允许您附加全局文档事件,如 scrollmousemovekeydown,然后在事件发生时执行定义的回调函数。

支持的文档事件列表。

该指令的语法为 data-wp-on-document--[document-event](例如 data-wp-on-document--keydowndata-wp-on-document--selectionchange)。

<div data-wp-on-document--keydown="callbacks.logKeydown"></div>
查看与上述指令一起使用的 store
store( 'myPlugin', {
    callbacks: {
        logKeydown( event ) {
            console.log( 'Key pressed: ', event.key );
        },
    },
} );

作为引用传递的回调函数接收 事件 (event),并且此回调的返回值将被忽略。当元素从 DOM 中移除时,事件监听器也会被移除。

wp-watch

当节点创建时运行回调函数,并在状态或上下文发生变化时再次运行。

你可以通过 data-wp-watch--[unique-id] 语法为同一个 DOM 元素附加多个副作用。

unique-id 不需要全局唯一,只需与该 DOM 元素的其他 wp-watch 指令的唯一 ID 不同即可。

<div data-wp-context='{ "counter": 0 }' data-wp-watch="callbacks.logCounter">
    <p>计数器: <span data-wp-text="context.counter"></span></p>
    <button data-wp-on--click="actions.increaseCounter">+</button>
    <button data-wp-on--click="actions.decreaseCounter">-</button>
</div>
查看与上述指令一起使用的 store
store( 'myPlugin', {
    actions: {
        increaseCounter: () => {
            const context = getContext();
            context.counter++;
        },
        decreaseCounter: () => {
            const context = getContext();
            context.counter--;
        },
    },
    callbacks: {
        logCounter: () => {
            const { counter } = getContext();
            console.log( 'Counter is ' + counter + ' at ' + new Date() );
        },
    },
} );

wp-watch 指令在以下情况下执行:

wp-watch 指令可以返回一个函数。如果返回了函数,该函数将用作清理逻辑,即在回调函数再次运行之前执行,并且在元素从 DOM 中移除时也会再次执行。

作为参考,该指令的一些用例可能包括:

如果你需要一个类似的响应式回调,但不绑定到特定的 DOM 元素,而是在 store 级别运行,可以使用 watch() 函数

wp-init

该指令仅在节点创建时运行回调函数。

你可以通过使用语法 data-wp-init--[unique-id] 在同一 DOM 元素上附加多个 wp-init 指令。

unique-id 不需要全局唯一,只需与该 DOM 元素上其他 wp-init 指令的唯一 ID 不同即可。

<div data-wp-init="callbacks.logTimeInit">
    <p>你好!</p>
</div>

以下是同一 DOM 元素上包含多个 wp-init 指令的另一个示例。

<form
    data-wp-init--log="callbacks.logTimeInit"
    data-wp-init--focus="callbacks.focusFirstElement"
>
    <input type="text" />
</form>
查看与上述指令一起使用的 store
import { store, getElement } from '@wordpress/interactivity';

store( "myPlugin", {
  callbacks: {
    logTimeInit: () => console.log( `Init at ` + new Date() ),
    focusFirstElement: () => {
      const { ref } = getElement();
      ref.querySelector( 'input:first-child' ).focus(),
    },
  },
} );

wp-init 可以返回一个函数。如果返回了函数,该函数将在元素从 DOM 中移除时执行。

wp-run

该指令会在节点渲染执行期间运行传入的回调函数。

你可以在传入的回调函数中使用和组合诸如 useStateuseWatchuseEffect 等钩子,并创建自己的逻辑,这比之前的指令提供了更大的灵活性。

你可以通过使用语法 data-wp-run--[unique-id] 将多个 wp-run 附加到同一个 DOM 元素上。

unique-id 不需要全局唯一,只需与该 DOM 元素上其他 wp-run 指令的唯一 ID 不同即可。

<div data-wp-run="callbacks.logInView">
    <p>Hi!</p>
</div>
查看与上述指令一起使用的 store
import {
    getElement,
    store,
    useState,
    useEffect,
} from '@wordpress/interactivity';

// 与 `data-wp-init` 和 `data-wp-watch` 不同,你可以在 `data-wp-run` 回调中使用任何钩子。
const useInView = () => {
    const [ inView, setInView ] = useState( false );
    useEffect( () => {
        const { ref } = getElement();
        const observer = new IntersectionObserver( ( [ entry ] ) => {
            setInView( entry.isIntersecting );
        } );
        observer.observe( ref );
        return () => ref && observer.unobserve( ref );
    }, [] );
    return inView;
};

store( 'myPlugin', {
    callbacks: {
        logInView: () => {
            const isInView = useInView();
            useEffect( () => {
                if ( isInView ) {
                    console.log( 'Inside' );
                } else {
                    console.log( 'Outside' );
                }
            } );
        },
    },
} );

需要注意的是,与 (P)React 组件类似,在首次渲染期间,getElement() 返回的 refnull。要正确访问 DOM 元素引用,通常需要使用类似效果的钩子,如 useEffectuseInituseWatch。这确保了 getElement() 在组件挂载且 ref 可用后运行。

wp-key

wp-key 指令为元素分配一个唯一键,以帮助 Interactivity API 在遍历元素数组时识别它们。如果数组元素可能移动(例如,由于排序)、被插入或被删除,这一点就变得非常重要。精心选择的键值有助于 Interactivity API 推断数组中具体发生了什么变化,从而使其能够对 DOM 进行正确的更新。

键应该是一个字符串,在其同级元素中唯一标识该元素。通常,它用于重复元素,如列表项。例如:

<ul>
    <li data-wp-key="unique-id-1">Item 1</li>
    <li data-wp-key="unique-id-2">Item 2</li>
</ul>

但它也可以用于其他元素:

<div>
    <a data-wp-key="previous-page" ...>Previous page</a>
    <a data-wp-key="next-page" ...>Next page</a>
</div>

当列表重新渲染时,Interactivity API 将通过元素的键来匹配它们,以确定是否添加/删除/重新排序了项目。没有键的元素可能会被不必要地重新创建。

wp-each

wp-each 指令用于渲染元素列表。该指令可在 <template> 标签中使用,其值指向存储在全局状态或上下文中的数组路径。<template> 标签内的内容将作为渲染每个项目的模板。

默认情况下,每个项目会以 item 为名称包含在上下文中,因此模板内的指令可以访问当前项目。

例如,考虑以下 HTML。

<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
    <template data-wp-each="context.list">
        <li data-wp-text="context.item"></li>
    </template>
</ul>

它将生成以下输出:

<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
    <li data-wp-text="context.item">hello</li>
    <li data-wp-text="context.item">hola</li>
    <li data-wp-text="context.item">olá</li>
</ul>

可以通过在指令名称后添加后缀来更改上下文中存储项目的属性名。在以下示例中,默认属性从 item 更改为 greeting

<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
    <template data-wp-each--greeting="context.list">
        <li data-wp-text="context.greeting"></li>
    </template>
</ul>

默认情况下,它使用每个元素作为渲染节点的键,但必要时也可以指定路径来获取键,例如当列表包含对象时。

为此,必须在 <template> 标签中使用 data-wp-each-key,而不是在模板内容中使用 data-wp-key。这是因为 data-wp-each 会为每个渲染的项目创建一个上下文提供者包装器,而这些包装器需要 key 属性。

<ul
    data-wp-context='{
  "list": [
    { "id": "en", "value": "hello" },
    { "id": "es", "value": "hola" },
    { "id": "pt", "value": "olá" }
  ]
}'
>
    <template
        data-wp-each--greeting="context.list"
        data-wp-each-key="context.greeting.id"
    >
        <li data-wp-text="context.greeting.value"></li>
    </template>
</ul>

wp-each-child

对于服务器端渲染的列表,另一个名为 data-wp-each-child 的指令确保水合过程按预期工作。当指令在服务器端处理时,此指令会自动添加。

<ul data-wp-context='{ "list": [ "hello", "hola", "olá" ] }'>
    <template data-wp-each--greeting="context.list">
        <li data-wp-text="context.greeting"></li>
    </template>
    <li data-wp-each-child>hello</li>
    <li data-wp-each-child>hola</li>
    <li data-wp-each-child>olá</li>
</ul>

指令的值是对存储属性的引用

分配给指令的值是一个指向特定状态、动作或副作用的字符串。

在以下示例中,使用 getter 来定义 state.isPlaying 派生值。

const { state } = store( 'myPlugin', {
    state: {
        currentVideo: '',
        get isPlaying() {
            return state.currentVideo !== '';
        },
    },
} );

然后,字符串值 "state.isPlaying" 被用于将此选择器的结果分配给 data-wp-bind--hidden

<div data-wp-bind--hidden="!state.isPlaying" ...>
    <iframe ...></iframe>
</div>

这些分配给指令的值是对存储中特定属性的引用。它们会自动连接到指令,因此每个指令都“知道”引用了哪个存储元素,无需任何额外配置。

请注意,默认情况下,引用指向当前命名空间中的属性,该命名空间由具有 data-wp-interactive 属性的最近祖先指定。如果需要访问来自不同命名空间的属性,可以显式设置所访问属性定义的命名空间。语法是 namespace::reference,将 namespace 替换为适当的值。

以下示例是从 otherPlugin 而非 myPlugin 获取 state.isPlaying

<div data-wp-interactive="myPlugin">
    <div data-wp-bind--hidden="otherPlugin::!state.isPlaying" ...>
        <iframe ...></iframe>
    </div>
</div>

存储

存储用于创建与指令相关的逻辑(操作、副作用等)以及该逻辑内部使用的数据(状态、派生状态等)。

存储通常在每个区块的 view.js 文件中创建,但状态可以从区块的 render.php 文件初始化。

Elements of the store

State

It defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data:

<div data-wp-context='{ "someText": "Hello World!" }'>
    <!-- Access global state -->
    <span data-wp-text="state.someText"></span>

    <!-- Access local state (context) -->
    <span data-wp-text="context.someText"></span>
</div>
const { state } = store( 'myPlugin', {
    state: {
        someText: 'Hello Universe!',
    },
    actions: {
        someAction: () => {
            state.someText; // Access or modify global state - "Hello Universe!"

            const context = getContext();
            context.someText; // Access or modify local state (context) - "Hello World!"
        },
    },
} );

Actions

Actions are just regular JavaScript functions. Usually triggered by the data-wp-on directive (using event listeners) or other actions.

const { state, actions } = store( 'myPlugin', {
    actions: {
        selectItem: ( id ) => {
            const context = getContext();
            // `id` is optional here, so this action can be used in a directive.
            state.selected = id || context.id;
        },
        otherAction: () => {
            // but it can also be called from other actions.
            actions.selectItem( 123 ); // it works and type is correct
        },
    },
} );
Async actions

Async actions should use generators instead of async/await.

In async functions, the control is passed to the function itself. The caller of the function has no way to know if the function is awaiting, and more importantly, if the await is resolved and the function has resumed execution. We need that information to be able to restore the scope.

Imagine a block that has two buttons. One lives inside a context that has isOpen: true and the other isOpen: false:

<div data-wp-context='{ "isOpen": true }'>
    <button data-wp-on--click="actions.someAction">Click</button>
</div>

<div data-wp-context='{ "isOpen": false }'>
    <button data-wp-on--click="actions.someAction">Click</button>
</div>

If the action is async and needs to await a long delay.

We need to be able to know when async actions start awaiting and resume operations, so we can restore the proper scope, and that's what generators do.

The store will work fine if it is written like this:

const { state } = store( 'myPlugin', {
    state: {
        get isOpen() {
            return getContext().isOpen;
        },
    },
    actions: {
        someAction: function* () {
            state.isOpen; // This context is correct because it's synchronous.
            yield longDelay(); // With generators, the caller controls when to resume this function.
            state.isOpen; // This context is correct because we restored the proper scope before we resumed the function.
        },
    },
} );

You may want to add multiple such yield points in your action if it is doing a lot of work.

如上文提到的 wp-onwp-on-windowwp-on-document 指令,当由于操作需要同步访问 event 对象而无法使用上述指令的 async 版本时,应使用异步操作。当操作需要调用 event.preventDefault()event.stopPropagation()event.stopImmediatePropagation() 时,就需要同步访问。

为确保操作代码不会导致长任务,你可以在调用同步事件 API 后手动让出主线程。Interactivity API 为此提供了 splitTask() 函数,它以跨浏览器兼容的方式实现了让出。示例如下:

import { splitTask } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        handleClick: withSyncEvent( function* ( event ) {
            event.preventDefault();
            yield splitTask();
            doTheWork();
        } ),
    },
} );

你可能注意到此示例中使用了 withSyncEvent() 工具函数。这是必要的,因为当前正在努力默认将存储操作作为异步处理,除非它们需要同步事件访问(本例中由于调用了 event.preventDefault() 而需要)。否则将触发弃用警告,并且在未来的版本中行为将相应改变。

副作用

自动响应状态变化。通常由 data-wp-watchdata-wp-init 指令触发。

派生状态

它们返回一个计算后的状态版本。可以访问 statecontext

// view.js
const { state } = store( 'myPlugin', {
    state: {
        amount: 34,
        defaultCurrency: 'EUR',
        currencyExchange: {
            USD: 1.1,
            GBP: 0.85,
        },
        get amountInUSD() {
            return state.currencyExchange[ 'USD' ] * state.amount;
        },
        get amountInGBP() {
            return state.currencyExchange[ 'GBP' ] * state.amount;
        },
    },
} );

在回调中访问数据

store 包含所有存储属性,如 stateactionscallbacks。它们由 store() 调用返回,因此您可以通过解构来访问它们:

const { state, actions } = store( 'myPlugin', {
    // ...
} );

store() 函数可以被多次调用,所有存储部分将被合并在一起:

store( 'myPlugin', {
    state: {
        someValue: 1,
    },
} );

const { state } = store( 'myPlugin', {
    actions: {
        someAction() {
            state.someValue; // = 1
        },
    },
} );
所有具有相同命名空间的 store() 调用都返回相同的引用,即相同的 stateactions 等,其中包含所有传入的存储部分合并后的结果。
const { state } = store( 'myPlugin', {
    state: {
        get someDerivedValue() {
            const context = getContext();
            const { ref } = getElement();
            // ...
        },
    },
    actions: {
        someAction() {
            const context = getContext();
            const { ref } = getElement();
            // ...
        },
    },
    callbacks: {
        someEffect() {
            const context = getContext();
            const { ref } = getElement();
            // ...
        },
    },
} );

这种方法实现了一些功能,使指令变得灵活而强大:

设置存储

在客户端

在每个区块的 view.js 文件中,开发者可以定义状态和存储的元素,引用诸如操作、副作用或派生状态等功能。

用于在 JavaScript 中设置存储的 store 方法可以从 @wordpress/interactivity 导入。

// 存储
import { store, getContext } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        toggle: () => {
            const context = getContext();
            context.isOpen = ! context.isOpen;
        },
    },
    callbacks: {
        logIsOpen: () => {
            const { isOpen } = getContext();
            // 每次 `isOpen` 变化时记录其值。
            console.log( `Is open: ${ isOpen }` );
        },
    },
} );

在服务器端

状态也可以在服务器端使用 wp_interactivity_state() 函数进行初始化。通常,您会在区块的 render.php 文件中执行此操作(render.php 模板是在 WordPress 6.1 中引入的)。

在服务器端使用 wp_interactivity_state() 定义的状态会与 view.js 文件中定义的存储合并。

wp_interactivity_state 函数接收两个参数:一个用作引用的命名空间 string,以及一个包含值的关联数组

从服务器初始化存储的示例,其中 state = { someValue: 123 }

// render.php
wp_interactivity_state( 'myPlugin', array (
    'someValue' => get_some_value()
));

在服务器端初始化状态还允许您使用任何 WordPress API。例如,您可以使用核心翻译 API 来翻译部分状态:

// render.php
wp_interactivity_state( 'favoriteMovies', array(
      "1" => array(
        "id" => "123-abc",
        "movieName" => __("someMovieName", "textdomain")
      ),
) );

Private stores

A given store namespace can be marked as private, thus preventing its content to be accessed from other namespaces. The mechanism to do so is by adding a lock option to the store() call, as shown in the example below. This way, further executions of store() with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first store() call. This is especially useful for developers who want to hide part of their plugin stores so it doesn't become accessible for extenders.

const { state } = store(
    'myPlugin/private',
    { state: { messages: [ 'private message' ] } },
    { lock: true }
);

// The following call throws an Error!
store( 'myPlugin/private', {
    /* store part */
} );

There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the lock value. Such a string can then be used in subsequent store() calls to the same namespace to unlock its content. Only the code knowing the string lock will be able to unlock the protected store namespaced. This is useful for complex stores defined in multiple JS modules.

const { state } = store(
    'myPlugin/private',
    { state: { messages: [ 'private message' ] } },
    { lock: PRIVATE_LOCK }
);

// The following call works as expected.
store(
    'myPlugin/private',
    {
        /* store part */
    },
    { lock: PRIVATE_LOCK }
);

客户端存储方法

除了存储函数外,还有一些方法允许开发者在存储函数中访问数据。

getContext()

Retrieves the context inherited by the element evaluating a function from the store. The returned value depends on the element and the namespace where the function calling getContext() exists. It can also take an optional namespace argument to retrieve the context of a specific interactive region.

const context = getContext( 'namespace' );
// render.php
<div data-wp-interactive="myPlugin" data-wp-context='{ "isOpen": false }'>
    <button data-wp-on--click="actions.log">Log</button>
</div>
// store
import { store, getContext } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        log: () => {
            const context = getContext();
            // Logs "false"
            console.log( 'context => ', context.isOpen );

            // With namespace argument.
            const myPluginContext = getContext( 'myPlugin' );
            // Logs "false"
            console.log( 'myPlugin isOpen => ', myPluginContext.isOpen );
        },
    },
} );

getElement()

获取操作绑定或调用的元素的表示形式。该表示形式为只读,包含对 DOM 元素的引用、其属性及本地响应式状态。 返回一个包含两个键的对象:

ref

ref 是对 DOM 元素的引用,类型为 HTMLElement。它等同于 Preact 或 React 中的 useRef,因此在 ref 尚未附加到实际 DOM 元素时(例如水合或挂载期间)可能为 null

attributes

attributes is an object that contains the attributes of the element. In the button example:

// store
import { store, getElement } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        log: () => {
            const element = getElement();
            // Logs attributes
            console.log( 'element attributes => ', element.attributes );
        },
    },
} );

The code will log:

{
    "data-wp-on--click": 'actions.log',
    "children": ['Log'],
    "onclick": event => { evaluate(entry, event); }
}

getServerContext()

This function is analogous to getContext(), but with 2 key differences:

  1. Whenever actions.navigate() from @wordpress/interactivity-router is called, the object returned by getServerContext() is updated. This is useful when you want to update the context of a block based on new context coming from the page loaded via actions.navigate(). This new context is embedded in the HTML of the page loaded via actions.navigate().
  2. The object returned by getServerContext() is read-only.

The server context cannot be directly used in directives, but you can use callbacks to subscribe to its changes.

const serverContext = getServerContext( 'namespace' );

Example usage:

store( 'myPlugin', {
    callbacks: {
        updateServerContext() {
            const context = getContext();
            const serverContext = getServerContext();
            // Override some property with the new value that came from the server.
            context.overridableProp = serverContext.overridableProp;
        },
    },
} );

getServerState()

Retrieves the server state of an interactive region.

This function serves the same purpose as getServerContext(), but it returns the state instead of the context.

The object returned is read-only, and includes the state defined in PHP with wp_interactivity_state(). When using actions.navigate() from @wordpress/interactivity-router, the object returned by getServerState() is updated to reflect the changes in its properties, without affecting the state returned by store(). Directives can subscribe to those changes to update the state if needed.

const serverState = getServerState( 'namespace' );

Example usage:

const { state } = store( 'myStore', {
    callbacks: {
        updateServerState() {
            const serverState = getServerState();
            // Override some property with the new value that came from the server.
            state.overridableProp = serverState.overridableProp;
        },
    },
} );

导航过程中服务器上下文与状态合并机制

在导航过程中,getServerContext()getServerState() 返回的数据会被新页面的值完全替换。与之相对,相关的客户端数据(上下文或状态)会进行"软合并"——保留现有的客户端属性,仅添加来自服务器的新属性。这确保了导航引入的新区块或组件能够用服务器提供的值初始化,同时用户所做的客户端更改得以保留。若需用服务器数据更新现有客户端属性(即覆盖值),请在回调函数内调用 getServerContext()getServerState() 并手动覆盖相关属性。

若在回调函数中订阅了 getServerContext()getServerState() 返回的任何值,该回调将在每次导航事件时被调用——无论该值是否发生变化。这使得在导航发生时能够根据需要可靠地重置或更新客户端数据。

withScope()

当操作被调用时,它们可能依赖于当前的作用域,例如,当你调用 getContext()getElement() 时。

当交互性 API 运行时执行回调时,作用域会自动设置。然而,如果你从不由运行时执行的回调中调用操作,例如在 setInterval() 回调中,你需要确保作用域被正确设置。在这些情况下,使用 withScope() 函数来确保作用域被正确设置。

一个例子,如果没有包装器,actions.nextImage 会触发未定义错误:

store( 'mySliderPlugin', {
    callbacks: {
        initSlideShow: () => {
            setInterval(
                withScope( () => {
                    actions.nextImage();
                } ),
                3_000
            );
        },
    },
} );

withSyncEvent()

Actions that require synchronous access to the event object need to use the withSyncEvent() function to annotate their handler callback. This is necessary due to an ongoing effort to handle store actions asynchronously by default, unless they require synchronous event access. Therefore, as of Gutenberg 20.4 / WordPress 6.8 all actions that require synchronous event access need to use the withSyncEvent() function. Otherwise a deprecation warning will be triggered, and in a future release the behavior will change accordingly.

Only very specific event methods and properties require synchronous access, so it is advised to only use withSyncEvent() when necessary. The following event methods and properties require synchronous access:

Here is an example, where one action requires synchronous event access while the other actions do not:

// store
import { store, withSyncEvent } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        // `event.preventDefault()` requires synchronous event access.
        preventNavigation: withSyncEvent( ( event ) => {
            event.preventDefault();
        } ),

        // `event.target` does not require synchronous event access.
        logTarget: ( event ) => {
            console.log( 'event target => ', event.target );
        },

        // Not using `event` at all does not require synchronous event access.
        logSomething: () => {
            console.log( 'something' );
        },
    },
} );

You can also use withSyncEvent() with generator functions (async actions). This is useful when you need both synchronous event access and asynchronous operations:

import { store, withSyncEvent } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        // Combining withSyncEvent with a generator function for async operations.
        navigate: withSyncEvent( function* ( event ) {
            event.preventDefault();
            const { actions } = yield import(
                '@wordpress/interactivity-router'
            );
            yield actions.navigate( event.target.href );
        } ),
    },
} );

watch()

订阅回调函数内部访问的任何信号的变更,每当这些信号变化时重新运行回调函数。返回一个清理函数用于停止监听。

data-wp-watch(这是一个与 DOM 元素生命周期绑定的指令)不同,watch() 函数是一个编程式 API,可以在 JavaScript 代码的任何位置使用,独立于 DOM。

import { store, watch } from '@wordpress/interactivity';

const { state } = store( 'myPlugin', {
    state: {
        counter: 0,
    },
    actions: {
        increment: () => {
            state.counter++;
        },
    },
} );

// 监听回调函数内部访问的任何状态的变更。
watch( () => {
    console.log( 'Counter is ' + state.counter );
} );

// 回调函数立即运行并记录 "Counter is 0"。
// 每次 `state.counter` 变化时,它将重新运行。

watch() 函数返回一个 unwatch 函数,调用该函数将停止监听器并阻止后续重新运行:

const unwatch = watch( () => {
    console.log( 'Counter is ' + state.counter );
} );

// 在稍后的某个时间点,当你想停止监听时:
unwatch();

传递给 watch() 的回调函数也可以返回一个清理函数。此清理函数会在回调函数因信号变更而重新执行之前运行,也会在通过 unwatch() 销毁监听器时运行:

const unwatch = watch( () => {
    const handler = () => { /* ... */ };
    document.addEventListener( 'click', handler );

    // 此清理函数在下一次重新执行之前运行,或在
    // 调用 `unwatch()` 时运行。
    return () => {
        document.removeEventListener( 'click', handler );
    };
} );

服务器函数

Interactivity API 提供了一系列便捷函数,使您能够在服务器端初始化和引用配置选项。这对于提供初始数据至关重要,服务器指令处理将利用这些数据在 HTML 标记发送到浏览器之前对其进行修改。这也是利用 WordPress 众多 API(如 nonce、AJAX 和翻译功能)的绝佳方式。

wp_interactivity_config

wp_interactivity_config 允许设置或获取一个配置数组,该数组引用到某个存储命名空间。 此配置在客户端也可用,但它是静态信息。

可以将其视为网站交互的全局设置,不会因用户交互而更新。

设置示例:

    wp_interactivity_config( 'myPlugin', array( 'showLikeButton' => is_user_logged_in() ) );

获取示例:

  wp_interactivity_config( 'myPlugin' );

此配置可在客户端获取:

// view.js

const { showLikeButton } = getConfig();

wp_interactivity_state

wp_interactivity_state allows the initialization of the global state on the server, which will be used to process the directives on the server and then will be merged with any global state defined in the client.

Initializing the global state on the server also allows you to use many critical WordPress APIs, including AJAX, or nonces.

The wp_interactivity_state function receives two arguments, a string with the namespace that will be used as a reference and an associative array containing the values.

Here is an example of passing the WP Admin AJAX endpoint with a nonce.

// render.php

wp_interactivity_state(
    'myPlugin',
    array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myPlugin_nonce' ),
    ),
);
// view.js

const { state } = store( 'myPlugin', {
    actions: {
        *doSomething() {
            try {
                const formData = new FormData();
                formData.append( 'action', 'do_something' );
                formData.append( '_ajax_nonce', state.nonce );

                const data = yield fetch( state.ajaxUrl, {
                    method: 'POST',
                    body: formData,
                } ).then( ( response ) => response.json() );
                console.log( 'Server data!', data );
            } catch ( e ) {
                // Something went wrong!
            }
        },
    },
} );

wp_interactivity_process_directives

wp_interactivity_process_directives 在处理完指令后返回更新后的 HTML。

它是 Interactivity API 服务器端渲染部分的核心函数,并且是公开的,因此任何 HTML(无论是否是区块)都可以被处理。

这段代码

wp_interactivity_state( 'myPlugin', array( 'greeting' => 'Hello, World!' ) );
$html_content = '<div data-wp-text="myPlugin::state.greeting"></div>';
$processed_html = wp_interactivity_process_directives( $html_content );
echo $processed_html;

将输出:

<div data-wp-text="myPlugin::state.greeting">Hello, World!</div>

wp_interactivity_data_wp_context

wp_interactivity_data_wp_context 返回一个上下文指令的字符串化 JSON。 此函数是在服务器端渲染标记中输出 data-wp-context 属性的推荐方式。

$my_context = array(
    'counter' => 0,
    'isOpen'  => true,
);
?>
<div
 <?php echo wp_interactivity_data_wp_context( $my_context ); ?>
>
</div>

将输出:

<div data-wp-context='{"counter":0,"isOpen":true}'></div>