Interactivity API 仅适用于 WordPress 6.5 及以上版本。

要使用 Interactivity API 为区块添加交互功能,开发者可以使用:

  • 指令(Directives):添加到标记中,以向区块的 DOM 元素添加特定行为
  • 存储(Store):包含行为所需的逻辑和数据(状态、操作、副作用等)

DOM 元素通过指令与存储在状态和上下文中的数据相连。如果状态或上下文中的数据发生变化,指令将响应这些变化并相应地更新 DOM(参见 图表)。

状态与指令

什么是指令?

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

Interactivity 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 Tag Processor 动态注入指令。

借助指令,您可以直接管理诸如副作用、状态、事件处理器、属性或内容等交互功能。

指令列表

wp-interactive

wp-interactive 指令通过 Interactivity API(指令和存储)“激活” DOM 元素及其子元素的交互功能。该指令包含一个命名空间,用于引用特定的 store,可以设置为 string 或 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>
使用 data-wp-interactive 是 Interactivity API “引擎” 正常工作的必要条件。在以下示例中,为了简单起见,未添加 data-wp-interactive。

wp-context

它为特定的 HTML 节点及其子节点提供本地状态。

wp-context 指令接受字符串化的 JSON 作为值。

// render.php
<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }' >
  <button data-wp-on--click="actions.logId" >
    Click Me!
  </button>
</div>
请参阅上述指令使用的 store
store( 'myPlugin', {
    actions: {
        logId: () => {
            const { post } = getContext();
            console.log( post.id );
        },
    },
} );

可以在不同级别定义不同的上下文,更深层级会将其自身的上下文与任何父级上下文合并:

<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

此指令允许基于布尔值或字符串值设置元素上的 HTML 属性。它遵循语法 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>
请参阅上述指令使用的 store
store( 'myPlugin', {
    actions: {
        toggleMenu: () => {
            const context = getContext();
            context.isMenuOpen = ! context.isMenuOpen;
        },
    },
} );

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

  • 当元素创建时
  • 每次 state 或 context 的任何属性发生变化,且这些属性涉及获取指令的最终值(在回调函数内或作为引用传递的表达式中)时

当 wp-bind 指令引用一个回调函数来获取其最终值时:

  • 每次 state 或 context 在此回调函数中使用的任何属性发生变化时,wp-bind 指令都会执行。
  • 回调函数返回的值用于更改关联属性的值。

根据值的类型,wp-bind 在应用于 DOM 元素时会执行不同的操作:

  • 如果值为 true,则添加属性: <div attribute>
  • 如果值为 false,则移除属性: <div>
  • 如果值为字符串,则添加属性并分配其值: <div attribute="value">
  • 如果属性名以 aria- 或 data- 开头,且值为布尔值(true 或 false),则将属性添加到 DOM 中,并将布尔值作为字符串分配: <div aria-attribute="true">

wp-class

此指令根据布尔值向 HTML 元素添加或删除类。它遵循语法 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>
请参阅上述指令使用的 store
store( 'myPlugin', {
    actions: {
        toggleSelection: () => {
            const context = getContext();
            context.isSelected = ! context.isSelected;
        },
    },
} );

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

  • 当元素创建时
  • 每次 state 或 context 的任何属性发生变化,且这些属性涉及获取指令的最终值(在回调函数内或作为引用传递的表达式中)时

指令接收到的布尔值用于切换(当 true 时添加,当 false 时移除) class 属性中的关联类名。

需要注意的是,在使用 wp-class 指令时,建议对类名使用 kebab-case(短横线连接)而不是 camelCase(驼峰命名)。这是因为 HTML 属性不区分大小写,HTML 会将 data-wp-class--isDark 视为与 data-wp-class--isdark 或 DATA-WP-CLASS--ISDARK 相同。

因此,例如,请使用类名 is-dark 而不是 isDark,以及 data-wp-class--is-dark 而不是 data-wp-class--isDark:

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

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

/* 不推荐 */
.isDark {
    /* ... */
}

wp-style

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

<div data-wp-context='{ "color": "red" }'>
    <button data-wp-on--click="actions.toggleContextColor">
        切换颜色文本
    </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 指令在以下情况执行:

  • 当元素被创建时
  • 每当涉及获取指令最终值的 state 或 context 中的任何属性发生变化时(在作为引用传递的回调函数或表达式内部)

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

  • 如果值为 false,则移除 style 属性:<div>
  • 如果值为字符串,则添加该属性并赋值:<div style="css-property: value;">

wp-text

设置 HTML 元素的内部文本。

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

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

  • 当元素被创建时
  • 每当涉及获取指令最终值的 state 或 context 中的任何属性发生变化时(在作为引用传递的回调函数或表达式内部)

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

wp-html

它设置元素的内部 HTML,渲染标记而不是纯文本 wp-text 所产生的内容。纯字符串、null 以及任何其他未明确标记为可信的值都会被忽略,从而保留元素的现有内容——这使得派生状态 getter 可以在加载内容时返回 null,并使服务器渲染的回退内容保持可见。

由于渲染任意 HTML 存在安全隐患,wp-html 仅接受从 @wordpress/interactivity 导出的 asDangerousHTML() 返回的值。直接传递普通字符串不会有任何效果,这是出于设计考虑:插件的 JavaScript 必须显式选择将该字符串渲染为标记。

<div data-wp-interactive="myPlugin">
    <article data-wp-html="state.description">
        <p>正在加载描述信息。</p>
    </article>
</div>
import { asDangerousHTML, store } from '@wordpress/interactivity';

store( 'myPlugin', {
    state: {
        get description() {
            return asDangerousHTML(
                '<p>一段<strong>格式化</strong>的描述。</p>'
            );
        },
    },
} );

安全契约

  • asDangerousHTML() 不进行清理。它对传入的 HTML 不进行任何处理——这与开发者使用 React 的 dangerouslySetInnerHTML 时所承担的责任相同。在此处将不可信输入视为可信输入存在 XSS 风险。切勿直接传递原始用户提供的内容或 context 值:context 旨在用于选择显示哪个可信标记(例如 ID 或标志),而实际的 HTML 应来自 state,由插件自身的代码从其信任的来源(其自身捆绑的标记或受控端点的响应)计算得出。
  • 返回值是不透明且基于身份的,而非基于内容的。它不包含其包装 HTML 的可检查痕迹。将其添加到 data-wp-context、展开到新对象中或序列化为 JSON 并不会随之携带“可信”标记——只有直接解析为 asDangerousHTML() 返回的确切值的 wp-html 引用才会被渲染。
  • 此包不会创建 Trusted Types 策略。在强制执行 Trusted Types 且没有默认策略的网站上,传递普通字符串会导致浏览器拒绝写入,并且元素的现有内容(服务器渲染的回退内容或上次成功渲染的 HTML)将保持不变。在这样的网站上渲染的支持方式是传递站点自身策略中的 TrustedHTML 值:asDangerousHTML( myPolicy.createHTML( markup ) )。此包故意不自行创建策略——这是由站点做出的决定。
  • “惰性”仅针对 Interactivity API 而言。插入到 HTML 中的 wp-html 指令内的子指令永远不会被处理,嵌套在其内部的交互式岛屿也不会被水合(嵌套的客户端导航区域尚未排除在处理之外)。但插入的内容仍然是活跃的浏览器 HTML:其中的 <script> 标签不会执行,但这只是 innerHTML 工作原理的一个副作用,而不是你应该依赖的安全边界。不适合以 HTML 形式渲染的内容通过 wp-html 处理后并不会变得安全。

服务器会跳过处理 wp-html,因此元素的服务器渲染内容可作为水合运行前的回退(如果 JavaScript 从未加载,或者在使用尚不支持此指令的 Interactivity API 版本时,该内容将保持可见)。

wp-on

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

该指令在分派的 DOM 事件(如 click 或 keyup)上运行代码。语法为 data-wp-on--[event](如 data-wp-on--click 或 data-wp-on--keyup)。

<button data-wp-on--click="actions.logTime">点击我!</button>
查看与上述指令配合使用的 store
store( 'myPlugin', {
    actions: {
        logTime: () => {
            console.log( new Date() );
        },
    },
} );

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

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

你可以通过使用 data-wp-on--[event]---[unique-id] 语法将多个回调附加到 DOM 元素的同一事件上(注意唯一 ID 之前的三个连字符)。data-wp-on-- 和三个连字符之间的所有内容都用作事件名称,因此 data-wp-on--click--counter 监听名为 click--counter 的事件,而不是 click。

<button
    data-wp-on--click="actions.logTime"
    data-wp-on--click---counter="actions.countClick"
>
    点击我!
</button>

wp-on-window

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

该指令允许你附加全局窗口事件(如 resize、copy 和 focus),并在这些事件发生时执行定义的回调。

支持的窗口事件列表。

此指令的语法为 data-wp-on-window--[window-event](如 data-wp-on-window--resize 或 data-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 中移除时,事件监听器也会被移除。

您可以通过使用 data-wp-on-window--[window-event]---[unique-id] 语法(注意唯一 ID 前的三个连字符)将多个回调函数附加到同一个窗口事件上,例如 data-wp-on-window--resize---log-width。

wp-on-document

默认情况下,此指令异步运行以提高性能。如果您需要同步访问 event 对象,请使用 withSyncEvent() 包装您的操作。对于也需要同步事件访问的异步操作,您可以将 withSyncEvent() 与 生成器函数 结合使用,该函数在调用同步 API 后会将控制权交还给主线程。

此指令允许您附加全局文档事件(如 scroll、mousemove 和 keydown),并在这些事件发生时执行定义的回调函数。

支持的文档事件列表。

此指令的语法为 data-wp-on-document--[document-event](例如 data-wp-on-document--keydown 或 data-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 中移除时,事件监听器也会被移除。

您可以通过使用 data-wp-on-document--[document-event]---[unique-id] 语法(注意唯一 ID 前的三个连字符)将多个回调函数附加到同一个文档事件上,例如 data-wp-on-document--keydown---log-key。

wp-watch

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

您可以通过使用 data-wp-watch---[unique-id] 语法(注意唯一 ID 前的三个连字符)将多个副作用附加到同一个 DOM 元素上。

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

<div data-wp-context='{ "counter": 0 }' data-wp-watch="callbacks.logCounter">
    <p>Counter: <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 指令会在以下情况执行:

  • 当元素被创建时
  • 每次回调函数内部使用的 state 或 context 的任何属性发生变化时

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

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

  • 日志记录
  • 更改页面标题
  • 使用 .focus() 设置元素的焦点
  • 在满足特定条件时更改状态或上下文

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

wp-init

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

您可以通过使用 data-wp-init---[unique-id] 语法(注意唯一 ID 前的三个连字符)将多个 wp-init 附加到同一个 DOM 元素上。

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

<div data-wp-init="callbacks.logTimeInit">
    <p>Hi!</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

此指令在节点的渲染执行期间运行传递的回调函数。

您可以在传递的回调函数内部使用和组合钩子,如 useState、useWatch 或 useEffect,并创建自己的逻辑,从而提供比先前指令更多的灵活性。

您可以通过使用 data-wp-run---[unique-id] 语法(注意唯一 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';

// Unlike `data-wp-init` and `data-wp-watch`, you can use any hooks inside
// `data-wp-run` callbacks.
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() 返回的 ref 在首次渲染期间为 null。要正确访问 DOM 元素引用,通常需要使用类似于 effect 的钩子,如 useEffect、useInit 或 useWatch。这可以确保 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>

可以通过向指令名称传递后缀来更改上下文中持有项目的 prop。在以下示例中,默认 prop 从 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>

! 运算符否定引用的值。它仅适用于值,如状态、上下文或使用 getter 定义的派生状态。如果引用指向函数(如操作或回调),则不会调用该函数,指令将接收 undefined(当启用 SCRIPT_DEBUG 时会发出警告)。要否定计算结果,请将其定义为带有 getter 的派生状态并否定该状态。

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

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

下面的示例是从 otherPlugin 获取 state.isPlaying,而不是从 myPlugin 获取:

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

Store

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

Store 通常在每个块的 view.js 文件中创建,尽管状态可以从块的 render.php 初始化。

Store 的元素

State

它定义了页面 HTML 节点可用的数据。区分两种定义数据的方式非常重要:

  • Global state:使用带有 state 属性的 store() 函数定义,数据对页面的所有 HTML 节点可用。
  • Context/Local State:使用 HTML 节点上的 data-wp-context 指令定义,数据对该 HTML 节点及其子节点可用。可以在操作、派生状态或副作用内部使用 getContext 函数访问它。
<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 只是普通的 JavaScript 函数。通常由 data-wp-on 指令(使用事件监听器)或其他 action 触发。

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 应使用生成器而不是 async/await。

在异步函数中,控制权传递给函数本身。函数的调用者无法知道函数是否正在等待,更重要的是,无法知道 await 是否已解决且函数已恢复执行。我们需要这些信息才能恢复作用域。

想象一个有两个按钮的块。一个位于具有 isOpen: true 的上下文中,另一个位于 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>

如果 action 是异步的并且需要等待长时间的延迟。

  • 用户点击第一个按钮。
  • 作用域指向第一个上下文,其中 isOpen: true。
  • 第一次访问 state.isOpen 是正确的,因为 state.isOpen 是一个调用 getContext() 的 getter,它返回当前作用域的上下文。
  • Action 开始等待长时间的延迟。
  • 在 action 恢复之前,用户点击第二个按钮。
  • 作用域更改为第二个上下文,其中 isOpen: false。
  • 第二个 action 中第一次访问 state.isOpen 是正确的,因为 getContext() 返回当前作用域的上下文。
  • 第二个 action 开始等待长时间的延迟。
  • 第一个 action 完成等待并恢复其执行。
  • 第一个 action 第二次访问 state.isOpen 是不正确的,因为作用域已被第二个按钮点击更改,所以 getContext() 现在返回错误的上下文。

我们需要能够知道异步操作何时开始等待并恢复执行,以便恢复正确的作用域,而生成器正是为此设计的。

const { state } = store( 'myPlugin', {
    state: {
        get isOpen() {
            return getContext().isOpen;
        },
    },
    actions: {
        someAction: function* () {
            state.isOpen; // 此上下文是正确的,因为它是同步的。
            yield longDelay(); // 使用生成器时,调用者控制何时恢复此函数。
            state.isOpen; // 此上下文是正确的,因为在恢复函数之前我们恢复了正确的作用域。
        },
    },
} );

如果您的操作执行大量工作,您可能需要在其中添加多个这样的 yield 点。

如上所述,对于 wp-on、wp-on-window 和 wp-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-watch 或 data-wp-init 指令触发。

派生状态

它们返回状态的计算版本。它们可以访问 state 和 context。

// 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 包含所有存储属性,如 state、actions 或 callbacks。它们由 store() 调用返回,因此您可以通过解构来访问它们:

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

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

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

const { state } = store( 'myPlugin', {
    actions: {
        someAction() {
            state.someValue; // = 1
        },
    },
} );
所有具有相同命名空间的 store() 调用返回相同的引用,即相同的 state、actions 等,包含传递的所有存储部分合并的结果。
  • 要在操作、派生状态或副作用内部访问上下文,您可以使用 getContext 函数。
  • 要访问引用,您可以使用 getElement 函数。
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();
            // ...
        },
    },
} );

这种方法启用了一些使指令灵活且强大的功能:

  • 操作和副作用可以读取和修改状态和上下文。
  • 块中的操作和状态可以被其他块访问。
  • 操作和副作用可以做任何常规 JavaScript 函数能做的事,例如访问 DOM 或发出 API 请求。
  • 副作用会自动响应状态更改。

设置存储

在客户端

在每个块的 view.js 文件中,开发人员可以定义存储的状态以及引用函数(如操作、副作用或派生状态)的元素。

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

// store
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。例如,您可以使用 Core Translation API 来翻译状态的一部分:

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

私有存储

给定的存储命名空间可以标记为私有,从而防止其内容被其他命名空间访问。实现这一点的机制是在 store() 调用中添加 lock 选项,如下面的示例所示。这样,对同一锁定命名空间的进一步 store() 执行将抛出错误,意味着该命名空间只能在其引用从第一次 store() 调用返回的地方访问。这对于想要隐藏部分插件存储以防止扩展程序可访问的开发人员特别有用。

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

// 以下调用会抛出 Error!
store( 'myPlugin/private', {
    /* store part */
} );

还有一种解锁私有存储的方法:除了传递布尔值外,您还可以使用字符串作为 lock 值。然后可以在对同一命名空间的后续 store() 调用中使用该字符串来解锁其内容。只有知道字符串锁的代码才能解锁受保护的存储命名空间。这对于在多个 JS 模块中定义的复杂存储很有用。

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

// 以下调用按预期工作。
store(
    'myPlugin/private',
    {
        /* store part */
    },
    { lock: PRIVATE_LOCK }
);

存储客户端方法

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

getContext()

获取由评估函数的元素从 store 继承的上下文。返回值取决于元素以及调用 getContext() 的函数所在的命名空间。它还可以接受一个可选的命名空间参数,以检索特定交互区域的上下文。

const context = getContext( 'namespace' );
  • 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 是一个包含元素属性的对象。在按钮示例中:

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

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

代码将记录:

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

getServerContext()

此函数类似于 getContext(),但有 2 个关键区别:

  1. 每当调用来自 @wordpress/interactivity-router 的 actions.navigate() 时,getServerContext() 返回的对象都会更新。当您希望基于通过 actions.navigate() 加载的页面传来的新上下文来更新块的上下文时,这非常有用。这个新上下文嵌入在通过 actions.navigate() 加载的页面的 HTML 中。
  2. getServerContext() 返回的对象是服务器上下文的深拷贝(快照),因此修改它不会影响原始的服务器数据。

服务器上下文不能直接在指令中使用,但您可以使用回调来订阅其更改。

const serverContext = getServerContext( 'namespace' );
  • namespace(可选):与交互区域命名空间匹配的字符串。如果未提供,则检索当前交互区域的服务器上下文。

用法示例:

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()

获取交互区域的服务器状态。

此函数的作用与 getServerContext() 相同,但它返回的是状态而不是上下文。

返回的对象是服务器状态的深拷贝(快照),因此修改它不会影响原始的服务器数据。它包括使用 wp_interactivity_state() 在 PHP 中定义的状态。当使用来自 @wordpress/interactivity-router 的 actions.navigate() 时,getServerState() 返回的对象会更新以反映其属性的变化,而不会影响 store() 返回的状态。指令可以订阅这些更改以根据需要更新状态。

const serverState = getServerState( 'namespace' );
  • namespace(可选):与交互区域命名空间匹配的字符串。如果未提供,则检索当前交互区域的服务器状态。

用法示例:

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()),操作可能依赖于作用域。

当 Interactivity API 运行时执行回调时,作用域会自动设置。但是,如果您从不由运行时执行的回调(如 setInterval() 回调)中调用操作,则需要确保作用域正确设置。使用 withScope() 函数以确保在这些情况下正确设置作用域。

以下是一个示例,其中如果没有包装器,actions.nextImage 将触发未定义错误:

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

withSyncEvent()

需要同步访问 event 对象的操作需要使用 withSyncEvent() 函数来注释其处理程序回调。这是由于目前正在努力默认异步处理存储操作,除非它们需要同步事件访问。因此,截至 Gutenberg 20.4 / WordPress 6.8,所有需要同步事件访问的操作都必须使用 withSyncEvent() 函数。否则将触发弃用警告,并且在未来的版本中行为将相应改变。

只有非常特定的事件方法和属性需要同步访问,因此建议仅在必要时使用 withSyncEvent()。以下事件方法和属性需要同步访问:

  • event.currentTarget
  • event.preventDefault()
  • event.stopImmediatePropagation()
  • event.stopPropagation()

以下是一个示例,其中一个操作需要同步事件访问,而其他操作则不需要:

// 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' );
        },
    },
} );

您还可以将 withSyncEvent() 与生成器函数(异步操作)一起使用。当您需要同时访问同步事件和执行异步操作时,这非常有用:

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()

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

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

即使您从具有作用域的 action 或回调中调用 watch(),回调也会在没有指令作用域的情况下运行。因此,getContext() 和 getElement() 在其内部不可用,调用它们会抛出异常,就像在 setTimeout() 回调中一样。对于仅读取 store 的工作,请使用 watch();当回调需要元素或其上下文时,请使用 data-wp-watch。

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

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

// Watch for changes to any state accessed inside the callback.
watch( () => {
    console.log( 'Counter is ' + state.counter );
} );

// The callback runs immediately and logs "Counter is 0".
// Each time `state.counter` changes, it will re-run.

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

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

// At a later point, when you want to stop watching:
unwatch();

传递给 watch() 的回调还可以返回一个清理函数。这个清理函数会在由于信号变化导致回调重新执行之前运行,也会在通过 unwatch() 销毁监听器时运行:

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

    // This cleanup runs before the next re-execution, or when
    // `unwatch()` is called.
    return () => {
        document.removeEventListener( 'click', handler );
    };
} );

Server functions

Interactivity API 提供了一些便捷函数,允许您在服务器上初始化并引用配置选项。这对于提供初始数据是必要的,服务器端指令处理(Server Directive Processing)将使用这些数据在将 HTML 标记发送到浏览器之前对其进行修改。这也是利用 WordPress 许多 API(如 nonces、AJAX 和翻译)的好方法。

wp_interactivity_config

wp_interactivity_config 允许您设置或获取一个配置数组,该数组引用了一个 store 命名空间。
配置信息也可以在客户端使用,但它是静态信息。

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

设置的示例:

    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 允许在服务器上初始化全局状态,该状态将用于在服务器上处理指令,然后与客户端定义的任何全局状态合并。

在服务器上初始化全局状态还允许您使用许多关键的 WordPress API,包括 AJAX 或 nonces。

wp_interactivity_state 函数接收两个参数:一个用作参考的命名空间字符串和一个包含值的关联数组。

以下是传递 WP Admin AJAX 端点和 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>