WordPress 始终建立在服务端渲染的基础之上。传统上,当用户请求一个 WordPress 页面时,服务器会处理 PHP 代码、查询数据库,并生成发送回浏览器的 HTML 标记。

近年来,Vue、React 或 Svelte 等现代 JavaScript 框架彻底改变了我们构建 Web 应用的方式。这些框架提供了响应式和声明式的编程模型,使开发人员能够轻松创建动态、交互式用户界面。

然而,就服务端渲染而言,这些框架需要一个基于 JavaScript 的服务器(如 NodeJS)来执行其代码并生成初始 HTML。这意味着像 WordPress 这样的基于 PHP 的服务器无法直接利用这些框架,而不会牺牲其原生的 PHP 渲染能力。这种局限性给希望利用响应式和声明式编程的强大功能,同时仍能受益于 WordPress 传统服务端渲染优势的 WordPress 开发人员带来了挑战。Interactivity API 通过将响应式和声明式编程原则引入 WordPress,而不损害其服务端渲染基础,从而弥合了这一差距。

在本指南中,我们将探讨 Interactivity API 如何在服务器上处理指令,使 WordPress 能够从初始页面加载开始提供具有交互性且感知状态的 HTML,同时为无缝的客户端交互奠定基础。

在服务器上处理指令

Interactivity API 的服务端指令处理功能使 WordPress 能够生成具有正确交互状态的初始 HTML,从而提供更快的初始渲染。在初始服务端渲染之后,Interactivity API 的客户端 JavaScript 接管,启用动态更新和交互,而无需进行整页刷新。这种方法结合了两者之所长:传统 WordPress 服务端渲染带来的 SEO 和性能优势,以及现代 JavaScript 框架提供的动态、响应式用户界面。

为了理解服务端指令处理的工作原理,让我们从一个使用 data-wp-each 指令呈现水果列表的示例开始。

以下是确保在 WordPress 服务端渲染期间,由 Interactivity API 的服务端指令处理正确处理的必要步骤。

  • 1. 将块标记为可交互

    首先,要启用对可交互块指令的服务端处理,您必须在 block.json 中添加 supports.interactivity:

    {
        "supports": {
            "interactivity": true
        }
    }
    
  • 2. 初始化全局状态或本地上下文

    然后,您必须初始化将在页面服务端渲染期间使用的全局状态或本地上下文。

    如果您使用的是全局状态,则必须使用 wp_interactivity_state 函数:

    wp_interactivity_state( 'myFruitPlugin', array(
      'fruits' => array( 'Apple', 'Banana', 'Cherry' )
    ));
    

    如果您使用的是本地上下文,则初始值由 data-wp-context 指令本身定义,可以通过以下方式:

    • 直接添加到 HTML 中。
      <ul data-wp-context='{ "fruits": ["Apple", "Banana", "Cherry"] }'>
          ...
      </ul>
      
    • 使用 wp_interactivity_data_wp_context 辅助函数。
      <?php
      $context = array( 'fruits' => array( 'Apple', 'Banana', 'Cherry' ) );
      ?>
      
      <ul <?php echo wp_interactivity_data_wp_context( $context ); ?>>
        ...
      </ul>
      
  • 3. 使用指令定义可交互元素

    接下来,您需要向 HTML 标记添加必要的指令。

    <ul data-wp-interactive="myFruitPlugin">
        <template data-wp-each="state.fruits">
            <li data-wp-text="context.item"></li>
        </template>
    </ul>
    

    在此示例中:

    • data-wp-interactive 指令激活 DOM 元素及其子元素的交互性。
    • data-wp-each 指令用于呈现元素列表。该指令可用于 <template> 标签,其值为存储在全局状态或本地上下文中的数组的引用路径。
    • data-wp-text 指令设置 HTML 元素的内部文本。在这里,它指向 context.item,这是 data-wp-each 指令存储数组中每个项的位置。

    在使用本地上下文而不是全局状态时,也可以使用完全相同的指令。唯一的区别是 data-wp-each 指向 context.fruits 而不是 state.fruits:

    <ul
        data-wp-interactive="myFruitPlugin"
        data-wp-context='{ "fruits": [ "Apple", "Banana", "Cherry" ] }'
    >
        <template data-wp-each="context.fruits">
            <li data-wp-text="context.item"></li>
        </template>
    </ul>
    

就是这样!一旦您使用 supports.interactivity 设置了可交互块,初始化了全局状态或本地上下文,并将指令添加到 HTML 标记中,Interactivity API 就会处理其余部分。开发人员无需编写额外代码即可在服务端处理这些指令。

在后台,WordPress 使用 wp_interactivity_process_directives 函数来查找和处理块 HTML 标记中的指令。该函数使用 HTML API 根据找到的指令以及初始全局状态和/或本地上下文对标记进行必要的更改。

因此,发送到浏览器的 HTML 标记已经是最终形式,所有指令都已正确处理。这意味着当页面首次在浏览器中加载时,它已经包含所有可交互元素的正确初始状态,无需任何 JavaScript 来修改它。

这就是水果列表示例的最终 HTML 标记的样子(省略了指令):

<ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Cherry</li>
</ul>

如您所见,data-wp-each 指令为数组中的每种水果生成了一个 <li> 元素,并且已处理 data-wp-text 指令,用正确的水果名称填充每个 <li>。

在客户端操纵全局状态和本地上下文

Interactivity API 的关键优势之一在于它如何弥合服务端渲染与客户端交互之间的差距。为此,在服务端初始化的全局状态和本地上下文也会被序列化,并可供客户端的 Interactivity API 存储使用,允许应用程序继续运行并动态操纵 DOM。

让我们扩展此示例,包括一个用户可以点击以向列表添加新水果的按钮:

<button data-wp-on--click="actions.addMango">Add Mango</button>

这个新按钮有一个 data-wp-on--click 指令,引用 actions.addMango,该指令在我们的 JavaScript 存储中定义:

const { state } = store( 'myFruitPlugin', {
    actions: {
        addMango() {
            state.fruits.push( 'Mango' );
        },
    },
} );

如果您使用的是本地上下文,同样的示例也可以工作:

store( 'myFruitPlugin', {
    actions: {
        addMango() {
            const context = getContext();
            context.fruits.push( 'Mango' );
        },
    },
} );

现在,当用户点击“Add Mango”按钮时:

  1. 触发 addMango 动作。
  2. 将 'Mango' 项添加到 state.fruits(或 context.fruits)数组中。
  3. Interactivity API 自动更新 DOM,为新水果添加一个新的 <li> 元素。
<ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Cherry</li>
    <li>Mango</li>
</ul>

请记住:如果已经在服务端完成了状态初始化,则在客户端初始化状态是不必要的。

store( 'myFruitPlugin', {
    state: {
        fruits: [ 'Apple', 'Banana', 'Cherry' ], // This is not necessary!
    },
} );

在服务端初始化派生状态

无论派生状态是从全局状态、本地上下文还是两者派生而来,都可以由服务端指令处理在服务端进行处理。

请访问Understanding global state, local context, derived state and config指南,了解更多关于 Interactivity API 中派生状态工作原理的信息。

可以静态定义的派生状态

让我们想象一下添加一个可以删除所有水果的按钮:

<button data-wp-on--click="actions.deleteFruits">Delete all fruits</button>
const { state } = store( 'myFruitPlugin', {
    actions: {
        // ...
        deleteFruits() {
            state.fruits = [];
        },
    },
} );

现在,当没有水果时,让我们显示一条特殊消息。为此,让我们使用 data-wp-bind--hidden 指令,该指令引用一个名为 state.hasFruits 的派生状态来显示/隐藏消息。

<div data-wp-interactive="myFruitPlugin">
    <ul data-wp-bind--hidden="!state.hasFruits">
        <template data-wp-each="state.fruits">
            <li data-wp-text="context.item"></li>
        </template>
    </ul>
    <div data-wp-bind--hidden="state.hasFruits">No fruits, sorry!</div>
</div>

派生状态 state.hasFruits 是在客户端使用 getter 定义的:

const { state } = store( 'myFruitPlugin', {
    state: {
        get hasFruits() {
            return state.fruits.length > 0;
        },
    },
    // ...
} );

到目前为止,客户端的一切都很正常,当我们按下“删除所有水果”按钮时,将显示“没有水果,抱歉!”的消息。问题在于,由于 state.hasFruits 未在服务器上定义,hidden 属性不会成为初始 HTML 的一部分,这意味着在 JavaScript 加载之前也会显示该消息,这不仅会让访问者感到困惑,还会在 JavaScript 最终加载并隐藏消息时导致布局偏移。

要修复此问题,您必须使用 wp_interactivity_state 在服务器上定义派生状态的初始值。

  • 如果初始值是已知且静态的,可以直接定义:
    wp_interactivity_state( 'myFruitPlugin', array(
      'fruits'    => array( 'Apple', 'Banana', 'Cherry' ),
      'hasFruits' => true
    ));
    
  • 或者可以通过进行必要的计算来定义:
    $fruits    = array( 'Apple', 'Banana', 'Cherry' );
    $hasFruits = count( $fruits ) > 0;
    
    wp_interactivity_state( 'myFruitPlugin', array(
      'fruits'    => $fruits,
      'hasFruits' => $hasFruits,
    ));
    

无论采用哪种方法,关键点在于 state.hasFruits 的初始值现在已在服务器上定义。这允许服务器指令处理(Server Directive Processing)处理 data-wp-bind--hidden 指令并修改 HTML 标记,在需要时添加 hidden 属性。

需要动态定义的派生状态

在大多数情况下,初始派生状态可以像前面的示例那样静态定义。但有时,值取决于也会在服务器上更改的动态值,并且需要在 PHP 中复制派生逻辑。

为了查看此示例,让我们继续为每个水果添加购物车表情符号(🛒),具体取决于它是否在购物清单上。

首先,让我们添加一个代表购物清单的数组。请记住,尽管出于简单起见这些数组是静态的,但通常您将处理动态信息,例如来自数据库的信息。

wp_interactivity_state( 'myFruitPlugin', array(
  'fruits'        => array( 'Apple', 'Banana', 'Cherry' ),
  'shoppingList'  => array( 'Apple', 'Cherry' ),
));

现在,让我们在客户端添加一个派生状态,检查每个水果是否在购物清单上,并返回相应的表情符号。

store( 'myFruitPlugin', {
    state: {
        get onShoppingList() {
            const context = getContext();
            return state.shoppingList.includes( context.item ) ? '🛒' : '';
        },
    },
    // ...
} );

然后使用该派生状态为每个水果显示适当的表情符号。

<ul data-wp-interactive="myFruitPlugin">
    <template data-wp-each="state.fruits">
        <li>
            <span data-wp-text="context.item"></span>
            <span data-wp-text="state.onShoppingList"></span>
        </li>
    </template>
</ul>

同样,到目前为止,客户端一切正常,访问者将看到其购物清单上水果的正确表情符号。然而,由于 state.onShoppingList 未在服务器上定义,表情符号不会成为初始 HTML 的一部分,并且在 JavaScript 加载之前不会显示。

让我们通过使用 wp_interactivity_state 添加初始派生状态来解决这个问题。请记住,这次值取决于来自 data-wp-each 指令的 context.item,这使得派生值成为动态的,因此让我们在 PHP 中复制 JavaScript 逻辑:

wp_interactivity_state( 'myFruitPlugin', array(
  // ...
  'onShoppingList' => function() {
    $state   = wp_interactivity_state();
    $context = wp_interactivity_get_context();
    return in_array( $context['item'], $state['shoppingList'] ) ? '🛒' : '';
  }
));

就这样!现在,我们的服务器可以计算派生状态,并知道哪些水果在购物清单上,哪些不在。这允许服务器指令处理用正确的值填充初始 HTML,确保用户即使在 JavaScript 运行时加载之前也能立即看到正确的信息。

序列化其他处理后的值以供客户端消费

wp_interactivity_state 函数对于从服务器发送处理后的值到客户端以便稍后消费也非常有价值。此功能在许多情况下很有用,例如管理翻译。

让我们在示例中添加翻译,看看这将如何工作。

<?php
wp_interactivity_state( 'myFruitPlugin', array(
  'fruits'         => array( __( 'Apple' ), __( 'Banana' ), __( 'Cherry' ) ),
  'shoppingList'   => array( __( 'Apple' ), __( 'Cherry' ) ),
  // ...
) );
?>

<div data-wp-interactive="myFruitPlugin">
  <button data-wp-on--click="actions.deleteFruits">
    <?php echo __( 'Delete all fruits' ); ?>
  </button>
  <button data-wp-on--click="actions.addMango">
    <?php echo __( 'Add Mango' ); ?>
  </button>
  <ul data-wp-bind--hidden="!state.hasFruits">
    <template data-wp-each="state.fruits">
      <li>
        <span data-wp-text="context.item"></span>
        <span data-wp-text="state.onShoppingList"></span>
      </li>
    </template>
  </ul>
  <div data-wp-bind--hidden="state.hasFruits">
    <?php echo __( 'No fruits, sorry!' ); ?>
  </div>
</div>

就这样!由于 Interactivity API 在 PHP 中运行,您可以直接将翻译添加到全局状态、本地上下文和 HTML 标记中。

但是等等,我们的 addMango 操作会发生什么?请记住,此操作仅在 JavaScript 中定义:

const { state } = store( 'myFruitPlugin', {
    actions: {
        addMango() {
            state.fruits.push( 'Mango' ); // Not translated!
        },
    },
} );

要解决此问题,您可以使用 wp_interactivity_state 函数序列化翻译后的 mango 字符串,然后在您的操作中访问该值。

wp_interactivity_state( 'myFruitPlugin', array(
  'fruits' => array( __( 'Apple' ), __( 'Banana' ), __( 'Cherry' ) ),
  'mango'  => __( 'Mango' ),
));
const { state } = store( 'myFruitPlugin', {
    actions: {
        addMango() {
            // `state.mango` contains the 'Mango' string already translated.
            state.fruits.push( state.mango );
        },
    },
} );

请注意,如果您的应用程序更加动态,您可以序列化包含所有水果翻译的数组,并在您的操作中仅使用水果关键字。例如:

wp_interactivity_state( 'myFruitPlugin', array(
  'fruits'           => array( 'apple', 'banana', 'cherry' ),
  'translatedFruits' => array(
    'apple'  => __( 'Apple' ),
    'banana' => __( 'Banana' ),
    'cherry' => __( 'Cherry' ),
    'mango'  => __( 'Mango' ),
  ),
  'translatedFruit'  => function() {
    $state   = wp_interactivity_state();
    $context = wp_interactivity_get_context();
    return $state['translatedFruits'][ $context['item'] ];
  }
));
const { state } = store( 'myFruitPlugin', {
  state: {
    get translatedFruit() {
      const context = getContext();
      return state.translatedFruits[ context.item ];
    }
  },
  actions: {
    addMango() {
      state.fruits.push( 'mango' );
    },
  },
} );
<template data-wp-each="state.fruits">
    <li data-wp-text="state.translatedFruit"></li>
</template>

从服务器传递非响应式、静态数据(例如 Ajax/REST-API URL 和非ces)也是一个常见需求。由于这些值在运行时永远不会改变,它们应属于 config 而不是全局状态。在服务端使用 wp_interactivity_config(),在客户端使用 getConfig()。

wp_interactivity_config( 'myPlugin', array(
  'ajaxUrl' => admin_url( 'admin-ajax.php' ),
  'nonce'   => wp_create_nonce( 'myPlugin_nonce' ),
));
import { store, getConfig } from '@wordpress/interactivity';

store( 'myPlugin', {
    actions: {
        *doSomething() {
            const { ajaxUrl, nonce } = getConfig();

            const formData = new FormData();
            formData.append( 'action', 'do_something' );
            formData.append( '_ajax_nonce', nonce );

            const data = yield fetch( ajaxUrl, {
                method: 'POST',
                body: formData,
            } ).then( ( response ) => response.json() );

            console.log( 'Server data', data );
        },
    },
} );

在传统主题中处理指令

只要你将 supports.interactivity 添加到你的 block.json 文件中,服务端指令处理就会在你的交互式区块中自动发生。但是对于传统主题呢?

传统主题也可以使用 Interactivity API,如果它们想利用服务端指令处理(这是应该做的),可以通过 wp_interactivity_process_directives 函数来实现。该函数接收包含未处理指令的 HTML 标记,并根据全局状态的初始值、本地上下文和派生状态返回修改后的 HTML 标记。

// 初始化全局和派生状态…
wp_interactivity_state( '...', /* ... */ );

// 包含指令的交互式 HTML 标记。
$html = '<div data-wp-...>...</div>';

// 处理指令,使其准备好发送到客户端。
$processed_html = wp_interactivity_process_directives( $html );

就是这样!你不需要做其他任何事情。

如果你想在模板文件中使用 wp_interactivity_process_directives,可以使用 ob_start 和 ob_get_clean 来捕获 HTML 输出并在渲染之前对其进行处理。

<?php
wp_interactivity_state( 'myClassicTheme', /* ... */ );
ob_start();
?>

<div data-wp-interactive="myClassicTheme">
  ...
</div>

<?php
$html = ob_get_clean();
echo wp_interactivity_process_directives( $html );

重要提示: 你只需要处理一次指令。如果你在一个模板中包含另一个内部模板文件,请确保仅在最高层的模板文件中调用 wp_interactivity_process_directives,以避免重复处理。

结论

Interactivity API 确保了从服务器渲染的内容到客户端交互的平滑且透明的过渡。你在服务器上定义的指令、初始全局状态或本地上下文以及客户端行为都构成了同一个生态系统的一部分。这种统一的方法简化了开发,提高了可维护性,并为创建交互式 WordPress 区块提供了更好的开发者体验。