Interactivity API 是一个响应式和声明式的框架,类似于 React、Vue、Svelte 或 Alpine 等其他现代框架。在使用 Interactivity API 时,采用正确的心态对于充分发挥其潜力至关重要。本指南将解释响应式(reactivity)和声明式(declarativeness)的核心概念,为有效使用 Interactivity API 奠定基础。

声明式与命令式

声明式编程描述了程序应该完成什么。它关注期望的结果,而不显式列出实现该结果所需的命令或步骤。相比之下,命令式编程通过明确陈述每一步来操作程序状态,从而指定如何完成任务。

命令式方法

在 Web 开发的早期,命令式方法是主流。这种方法涉及使用 JavaScript 手动更新 DOM 以反映更改。

例如,考虑这个包含两个按钮和一个段落的交互式区块:

  • 显示/隐藏按钮:切换段落的可见性,并启用/禁用“激活”按钮。
  • 激活/停用按钮:在“激活”(绿色)和“非激活”(红色)之间切换段落的文本和颜色。
<div id="my-interactive-plugin">
    <button
        id="show-hide-btn"
        aria-expanded="false"
        aria-controls="status-paragraph"
    >
        show
    </button>
    <button id="activate-btn" disabled>activate</button>
    <p id="status-paragraph" class="inactive" hidden>this is inactive</p>
</div>

<style>
    .active {
        color: green;
    }
    .inactive {
        color: red;
    }
</style>

<script>
    const showHideBtn = document.getElementById( 'show-hide-btn' );
    const activateBtn = document.getElementById( 'activate-btn' );
    const statusParagraph = document.getElementById( 'status-paragraph' );

    showHideBtn.addEventListener( 'click', () => {
        if ( statusParagraph.hasAttribute( 'hidden' ) ) {
            statusParagraph.removeAttribute( 'hidden' );
            showHideBtn.textContent = 'hide';
            showHideBtn.setAttribute( 'aria-expanded', 'true' );
            activateBtn.removeAttribute( 'disabled' );
        } else {
            if ( statusParagraph.classList.contains( 'active' ) ) {
                statusParagraph.textContent = 'this is inactive';
                statusParagraph.classList.remove( 'active' );
                activateBtn.textContent = 'activate';
            }
            statusParagraph.setAttribute( 'hidden', true );
            showHideBtn.textContent = 'show';
            showHideBtn.setAttribute( 'aria-expanded', 'false' );
            activateBtn.setAttribute( 'disabled', true );
        }
    } );

    activateBtn.addEventListener( 'click', () => {
        if ( activateBtn.textContent === 'activate' ) {
            statusParagraph.textContent = 'this is active';
            statusParagraph.classList.remove( 'inactive' );
            statusParagraph.classList.add( 'active' );
            activateBtn.textContent = 'deactivate';
        } else {
            statusParagraph.textContent = 'this is inactive';
            statusParagraph.classList.remove( 'active' );
            statusParagraph.classList.add( 'inactive' );
            activateBtn.textContent = 'activate';
        }
    } );
</script>

正如你所见,对于每种情况,你都不得不使用 JavaScript 来修改 DOM 中发生变化的所有部分,同时要考虑之前的状态。

声明式方法

声明式方法通过专注于什么应该发生来简化过程。UI 会根据状态的变化自动更新。以下是使用 Interactivity API 的声明式方法的类似示例:

<div id="my-interactive-plugin" data-wp-interactive="myInteractivePlugin">
    <button
        data-wp-on--click="actions.toggleVisibility"
        data-wp-bind--aria-expanded="state.isVisible"
        data-wp-text="state.visibilityText"
        aria-controls="status-paragraph"
    >
        show
    </button>
    <button
        data-wp-on--click="actions.toggleActivation"
        data-wp-bind--disabled="!state.isVisible"
        data-wp-text="state.activationText"
    >
        activate
    </button>
    <p
        id="status-paragraph"
        data-wp-bind--hidden="!state.isVisible"
        data-wp-class--active="state.isActive"
        data-wp-class--inactive="!state.isActive"
        data-wp-text="state.paragraphText"
    >
        this is inactive
    </p>
</div>

<style>
    .active {
        color: green;
    }
    .inactive {
        color: red;
    }
</style>
import { store } from '@wordpress/interactivity';

const { state } = store( 'myInteractivePlugin', {
    state: {
        isVisible: false,
        isActive: false,
        get visibilityText() {
            return state.isVisible ? 'hide' : 'show';
        },
        get activationText() {
            return state.isActive ? 'deactivate' : 'activate';
        },
        get paragraphText() {
            return state.isActive ? 'this is active' : 'this is inactive';
        },
    },
    actions: {
        toggleVisibility() {
            state.isVisible = ! state.isVisible;
            if ( ! state.isVisible ) state.isActive = false;
        },
        toggleActivation() {
            state.isActive = ! state.isActive;
        },
    },
} );

在这个声明式示例中,UI 会根据当前状态自动更新。作为开发者,你需要做的就是声明必要的状态、任何派生状态、修改状态的 action,以及 DOM 的哪些部分依赖于状态的哪些部分。框架负责对所有 DOM 进行必要的更新,使其始终与当前状态保持同步。无论由框架控制多少元素,逻辑都保持简单且易于维护。

你能发现这个 bug 吗?

在命令式示例中,为了教学目的故意引入了一个 bug。你能找到它吗?这并不容易!

Show me the answer!

如果先按下 Show 按钮,然后按 Activate 按钮,最后按 Hide 按钮,它不会使用 statusParagraph.classList.add('inactive'); 添加 inactive 类。因此,下次用户按下 Show 时,段落将不会以红色显示。

这类 bug 在命令式代码中非常常见,因为你必须手动控制所有条件。另一方面,在声明式代码中它们不存在,因为框架负责更新 DOM,永远不会遗漏任何内容。

声明式方法的优势

如上所示,命令式方法需要详细的步骤和对 DOM 的直接操作,随着交互复杂性的增加,这可能会迅速变得复杂且难以维护。可能的状态和元素越多,就需要添加的条件逻辑越多,从而使代码呈指数级复杂化。相反,声明式方法通过管理状态并让框架处理 DOM 更新来简化过程。这导致了更具可读性、可维护性和可扩展性的代码。

响应式

Interactivity API 是一个声明式框架,这是因为它利用了响应式机制。在响应式系统中,数据的变化会自动触发用户界面的更新,确保视图始终反映应用程序的当前状态。

响应式的工作原理

Interactivity API 使用细粒度的响应式系统。其工作原理如下:

  1. 响应式状态:在 Interactivity API 中,全局状态和本地上下文都是响应式的。这意味着当这些数据源中的任何一个发生变化时,依赖它们的任何 UI 部分都会自动更新。
    • 全局状态:这是在您的交互式块中可以访问的全局数据。
    • 本地上下文:这是特定于某个元素及其子元素的本地数据。
    • 派生状态:除了基本状态属性外,您还可以定义计算属性,当它们的依赖项发生变化时,这些属性会自动更新。

    请访问Understanding global state, local context, derived state and config指南,了解如何在 Interactivity API 中使用不同类型的响应式状态。

  2. Actions:这些是通常由事件处理器触发的函数,用于修改全局状态或本地上下文。

  3. 响应式绑定:HTML 元素使用特殊属性(如 data-wp-bind、data-wp-text 或 data-wp-class)绑定到响应式状态值。

  4. 自动更新:当 actions 修改全局状态或本地上下文时,Interactivity API 会自动更新 DOM 中依赖该状态的所有部分(无论是直接还是通过派生状态)。

让我们通过回顾前面的例子来分解这些概念:

const { state } = store( 'myInteractivePlugin', {
    state: {
        isVisible: false,
        isActive: false,
        get visibilityText() {
            return state.isVisible ? 'hide' : 'show';
        },
        // ... other derived state
    },
    actions: {
        toggleVisibility() {
            state.isVisible = ! state.isVisible;
        },
        // ... other actions
    },
} );

在这段代码中:

  • isVisible 和 isActive 是基本状态属性。
  • visibilityText 是一个派生状态,当 isVisible 更改时会自动更新。
  • toggleVisibility 是一个修改状态的操作(action)。

HTML 绑定如下所示:

<button
    data-wp-on--click="actions.toggleVisibility"
    data-wp-text="state.visibilityText"
    data-wp-bind--aria-expanded="state.isVisible"
>
    show
</button>

以下是响应式机制在实际中的工作原理:

  1. 当点击按钮时,它会触发 toggleVisibility 操作。
  2. 此操作会更新 state.isVisible。
  3. Interactivity API 检测到这一变化并自动执行以下操作:
    • 更新按钮的文本内容(因为存在 data-wp-text="state.visibilityText")。
    • 更改 aria-expanded 属性(由于 data-wp-bind--aria-expanded="state.isVisible")。
    • 更新任何依赖于 isVisible 或 visibilityText 的其他 DOM 部分。

可变性与不可变性

与许多其他响应式框架不同,在更新全局状态或本地上下文时,Interactivity API 不要求使用不可变性。你可以直接修改对象和数组,响应式系统仍会按预期工作。在许多情况下,这可以使代码更加直观和简单。

例如,你可以像这样向数组中推送新项:

const { state } = store( 'myArrayPlugin', {
    state: {
        list: [ 'item 1', 'item 2' ],
    },
    actions: {
        addItem() {
            // 推荐做法 — 直接变异:
            state.list.push( 'new item' );

            // 不必要 — 这样可以工作,但更复杂且可能导致不必要的重新渲染。
            state.list = [ ...state.list, 'new item' ];
        },
    },
} );

无需像在其他框架中那样创建新数组或使用展开运算符。Interactivity API 将检测到此更改并更新 UI 中依赖 state.list 的任何部分。

响应式副作用

除了自动更新 UI 外,Interactivity API 还允许你使用如 data-wp-watch 之类的指令在响应式数据发生变化时执行副作用。副作用对于记录日志、进行 API 调用或更新应用程序中不与 UI 直接相关的其他部分等任务非常有用。

以下是如何使用 data-wp-watch 的示例:

<div
    data-wp-interactive="myCounterPlugin"
    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.increment">Increment</button>
</div>
import { store, getContext } from '@wordpress/interactivity';

store( 'myCounterPlugin', {
    actions: {
        increment() {
            const context = getContext();
            context.counter += 1;
        },
    },
    callbacks: {
        logCounter: () => {
            const context = getContext();
            console.log( `The counter is now: ${ context.counter }` );
        },
    },
} );

在此示例中:

  1. data-wp-context 指令添加了一个包含属性 counter 的本地上下文,其值为 0。
  2. data-wp-watch 指令设置为 callbacks.logCounter。
  3. 每次 context.counter 更改时,都会执行 logCounter 回调函数。
  4. logCounter 回调函数会将当前计数器值记录到控制台。

这使得你可以创建声明式的副作用,这些副作用会在数据更改时自动运行。data-wp-watch 的其他一些用例可能包括:

  • 当数据更改时将数据保存到 localStorage。
  • 发送分析事件。
  • 出于无障碍目的更改焦点。
  • 更新页面标题、元标签或 <body> 属性。
  • 触发动画。

结论

随着你继续使用 Interactivity API,请记住要从状态、操作和副作用的角度进行思考。定义你的数据,描述它应如何更改,并让 Interactivity API 处理其余部分。这种思维转变可能需要一些时间,特别是如果你习惯于更命令式的编程风格,但通过拥抱它,你将解锁 Interactivity API 的全部潜力,从而创建真正动态且交互式的 WordPress 区块,让你的用户感到愉悦。