客户端导航通过仅更新页面中发生变化的部分,实现无需完整页面刷新的页面过渡。为了使其正常工作,页面上的每个交互区域都必须与客户端导航兼容。如果任何交互区域不兼容,它在导航后可能会无声地失效——导致状态丢失、初始化失败或渲染错误。

一些核心区块(如 core/query)能够检测不兼容的后代元素并自动回退到完整页面刷新,但这种安全机制不适用于自定义实现。通常,页面上单个不兼容的交互区域就可能导致整个导航体验受损。

本指南解释了在评估兼容性时需要考虑的事项。虽然示例侧重于交互式区块,但相同的原则适用于前端上的任何交互区域——包括经典 PHP 主题或插件中包含的区域。

本指南假设您熟悉 区块、block.json 以及 Interactivity API 的基础知识。如果您是 Interactivity API 的新手,请先阅读 快速入门指南。

如需了解客户端导航的工作原理概述——包括 fetch-diff-apply 循环、路由器区域以及样式和脚本模块处理——请参阅 客户端导航指南。

在 block.json 中声明兼容性

区块通过在 block.json 中使用 supports.interactivity.clientNavigation 属性来声明对客户端导航的兼容性:

{
    "supports": {
        "interactivity": {
            "clientNavigation": true
        }
    }
}

将 clientNavigation 设置为 true 以表示该区块在客户端导航期间能正常工作。如果未声明,则默认为 false,意味着该区块被视为不兼容。

如果区块还使用了 Interactivity API 指令,请同时声明这两个属性:

{
    "supports": {
        "interactivity": {
            "clientNavigation": true,
            "interactive": true
        }
    }
}

将 supports.interactivity 设置为 true 是同时将 clientNavigation 和 interactive 设置为 true 的简写形式:

{
    "supports": {
        "interactivity": true
    }
}

即使一个区块看起来在客户端导航中工作正常,也必须在 `block.json` 中明确声明其兼容性。目前,这是 WordPress(以及其他区块、插件和主题)确定您的区块是否与客户端导航兼容的唯一方式;如果不兼容,则可以禁用它。例如,`core/query` 区块会检查其所有后代区块的这个属性,并在其中任何一个不兼容时回退到全页刷新。未来,此检查可能会被自动集成到 Interactivity API 本身中。

什么使区块具备兼容性

非交互式区块

呈现 HTML 且没有任何客户端交互性(无 JavaScript、无事件监听器、无动态行为)的区块与客户端导航兼容。由于它们不依赖脚本来运行功能,客户端导航可以安全地替换它们的 HTML 而不会破坏任何东西。

然而,兼容性仍必须在 block.json 中显式声明。Interactivity API 无法自行推断兼容性。

使用 Interactivity API 的交互式区块

使用 Interactivity API 处理其客户端行为的区块旨在与客户端导航配合工作。Interactivity API 通过其 虚拟 DOM 差异算法 管理 DOM 更新,确保在导航过程中保留交互状态。

话虽如此,交互式区块必须遵循某些实践以保持兼容性。这些指南涵盖三个领域——CSS、JavaScript 和 HTML——详见 确保兼容性。

这同样适用于包含在经典 PHP 主题或插件中的交互区域。

使用原生 JavaScript (DOM API) 或其他交互库的交互式区块

使用原生 JavaScript、jQuery 或除 Interactivity API 之外的任何框架来处理客户端行为的区块不兼容客户端导航。对于这些区块,请将 clientNavigation 设置为 false(或省略它)。

这些区块通常依赖于在页面加载时运行一次的脚本来初始化行为——附加事件监听器、操作 DOM 或设置小部件。在客户端导航期间,HTML 可能会被替换,但这些初始化脚本不会再次运行,实际 DOM 状态的内部 JS 表示可能会不同步,导致区块无法 functioning。

快速参考

下表总结了每种区块类型的兼容性状态和所需操作:

区块类型兼容?操作
非交互式区块(无 JS)是将 clientNavigation 设置为 true
使用 Interactivity API 的交互式区块是(如果遵循指南)将 interactivity 设置为 true(简写形式)
使用其他交互库的交互式区块否省略或将 clientNavigation 设置为 false
在运行时注入或修改 CSS 的区块否使用带有 data-wp-class 的服务器端样式
使用 wp_unique_id() 作为 CSS 选择器的区块否使用 wp_unique_id_from_values()
使用常规脚本(而非脚本模块)的区块否迁移到脚本模块
从 window.wp.* 全局变量导入的区块否使用 ES 模块导入
依赖 DOM ready 事件进行初始化的区块否使用 data-wp-init
在 Interactivity API 之外修改 DOM 的区块否使用指令或 data-wp-watch

这同样适用于包含在经典 PHP 主题或插件中的交互区域。

确保兼容性

使用 Interactivity API 的交互式区块和区域需要遵循某些指南才能与客户端导航正确工作。以下各节按问题类型组织:CSS、JavaScript 和 HTML。

CSS

不要动态注入 CSS

通过 JavaScript 在运行时注入 <style> 或 <link> 元素的区块与客户端导航不兼容。

Interactivity API 通过跟踪服务器在页面中包含的 <link> 和 <style> 元素来在导航期间管理样式表。由客户端代码动态创建的样式超出了此系统的范围——导航后,区块可能看起来没有样式或布局损坏。

替代方案:使用服务器端逻辑在渲染区块时包含适当的样式表。例如,使用 block.json 的 style 属性,或在区块的 render 模板中有条件地入队样式表。

不要使用 JavaScript 修改现有样式表

在运行时以编程方式修改 CSS 规则与客户端导航不兼容。这包括使用 CSSOM API 如 CSSStyleSheet.insertRule()、CSSStyleSheet.deleteRule(),或直接修改 CSSStyleDeclaration 对象。这些更改不会被 Interactivity API 跟踪,并且当页面的样式表在导航期间进行协调时会丢失。

替代方案:使用服务器端逻辑生成正确的样式表,或使用 Interactivity API 指令(如 data-wp-class 或 data-wp-style)切换元素上的 CSS 类。

使用稳定的 CSS 选择器

CSS 选择器(类名、ID 等)必须在导航之间保持稳定。如果选择器在页面加载之间发生变化,样式可能会在客户端导航后损坏或应用到错误的元素上。

这对于位于路由器区域之外的元素尤为重要(路由器区域是 Interactivity API 在导航期间替换的页面区域)。由于这些区域之外的元素不会被替换,新页面的样式表必须继续匹配它们。如果两个页面共享相同的模板,通常情况如此,但当不同的模板为标头、页脚或侧边栏等共享布局元素生成不同的包装元素或类名时,可能会出现不匹配的情况。

应用于路由器区域内元素的 CSS 选择器也必须保持稳定。由于区域在导航期间会被替换,传入的 HTML 必须使用相同的选择器,以便现有样式表继续正确应用。

导致客户端导航兼容性失败的最常见原因之一是 wp_unique_id()。此函数基于全局计数器生成连续 ID(id-1、id-2 等),该计数器在每次页面加载时重置。当在两个页面之间导航时,同一个块可能在每个页面上获得不同的 ID,导致 CSS 选择器不再匹配该元素。

相反,请使用 wp_unique_id_from_values()(自 WordPress 6.8 起可用)。此函数从值数组生成基于哈希的确定性标识符,无论渲染顺序如何,都为相同的输入产生相同的 ID:

// 避免:连续 ID 在页面间发生变化。
$id = wp_unique_id( 'my-block-' );

// 推荐:基于哈希的 ID 在导航过程中保持稳定。
$id = wp_unique_id_from_values(
    array( $block->parsed_block['attrs'] ),
    'my-block-'
);

这适用于 CSS 中使用的任何选择器——类名、ID 或样式表中使用的 data-* 属性。

JavaScript

使用脚本模块,而非普通脚本

客户端导航仅支持 脚本模块(<script type="module">)。

普通脚本在通过客户端导航访问页面时不会重新执行——因此,例如 jQuery 滑块或原生 JS 手风琴组件仅在首次页面加载时包含才会初始化,但如果仅在后续导航中包含,则不会初始化。

脚本模块和客户端导航的关键规则如下:

  • 仅支持 <script type="module"> 标签。没有 type="module" 属性的普通 <script> 标签在导航期间会被忽略。
  • 仅处理外部脚本模块(具有 src 属性的模块)。内联脚本模块——即代码直接写在 <script> 标签内部——不会重新执行。
  • Interactivity API 仅加载包含 data-wp-router-options='{"loadOnClientNavigation":true}' 属性的模块。
    • 对于块,当脚本模块在 block.json 中声明且 supports.interactivity 或 supports.interactivity.clientNavigation 为 true 时,会自动设置 loadOnClientNavigation 标志。
    • 对于非块脚本模块(例如由主题或插件排队的脚本模块),请使用 add_client_navigation_support_to_script_module() 显式注册以用于客户端导航。
wp_interactivity()->add_client_navigation_support_to_script_module(
    'my-plugin/navigation-handler'
);

有关脚本模块在导航期间如何处理更多详细信息,请参阅客户端导航指南中的脚本模块处理部分。

不要从 window.wp.* 全局变量导入

通过 window.wp.element 或 window.wp.data 等全局变量访问 WordPress 包与客户端导航不兼容。这些全局变量由普通脚本设置,而普通脚本在客户端导航期间不会重新执行。

替代方案:如果存在对应的 @wordpress/* 脚本模块,请使用 ES 模块导入。例如,@wordpress/a11y 既作为普通脚本(window.wp.a11y)可用,也作为脚本模块可用:

// 避免:依赖于由普通脚本设置的全局变量。
const { speak } = window.wp.a11y;

// 推荐:ES 模块导入,由脚本模块系统解析。
import { speak } from '@wordpress/a11y';

不要依赖 DOM ready 事件进行初始化

块的水合和初始化代码不应依赖于 DOMContentLoaded 或 load 等 DOM ready 事件。这些事件仅在初始完整页面加载时触发一次,在客户端导航后不会再次触发。这些监听器内的任何设置逻辑在用户导航到新页面时都不会运行。

替代方案:对于需要在块每次进入页面时运行的代码,请使用 data-wp-init 指令:

<div data-wp-interactive="myPlugin" data-wp-init="callbacks.setup">
    Block content
</div>

对于需要每当 URL 更改时重新运行的代码——例如分析页面浏览跟踪——请使用带有全局状态中响应值的 data-wp-watch,如当前 URL:

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

store( 'myPlugin', {
    callbacks: {
        logPageView() {
            // 每当 state.url 更改时重新运行。
            const { url } = store( 'core/router' ).state;
            // 发送新 URL 的分析事件。
            sendPageView( url );
        },
    },
} );
<div data-wp-interactive="myPlugin" data-wp-watch="callbacks.logPageView">
    Tracked content
</div>

从 WordPress 7.0 开始,您还可以使用 watch 实用程序在 URL 更改时重新执行代码。

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

watch( () => {
    const { url } = store( 'core/router' ).state;
    // 发送新 URL 的分析事件。
    sendPageView( url );
} );

请注意,state.url 在您已经所在的页面的导航中不会更改,例如用于在表单提交后显示新内容的 navigate( window.location.href, { force: true } ) 模式,因此上述两个示例在这种情况下均不会运行。必须在每次导航时运行的代码应改为观察路由器的 state.navigating 属性。请参阅客户端导航指南中的对导航生命周期的响应,以及导航后的区域范围焦点,后者是一个在每个导航结束时恰好运行一次的回调。

还要注意,上述两个示例看到的 DOM 并不相同。data-wp-watch 回调被延迟一帧,并在新内容渲染后运行,而 watch() 回调在 state.url 更改后立即同步运行,在区域重新渲染之前,因此它仍然看到上一页。这对页面浏览跟踪没有影响,但任何读取或测量新内容的回调应在导航结束时运行。请参阅选择读取键的位置。此类回调还应自行查找元素并检查它们是否存在,因为渲染时抛出异常的导航或回退到完整页面加载的导航在没有新内容的情况下结束;请参阅当导航在没有新内容的情况下结束时。

使用 getServerState() 和 getServerContext() 同步服务器数据

如果您的块的状态或上下文需要在每次导航时反映服务器渲染的值——例如,重置计数器、更新标签或同步随页面变化的数据——请在回调中使用 getServerState() 和 getServerContext()。这些函数返回新导航页面的服务器渲染 HTML 中的值,允许您在每次导航后将客户端状态与新的服务器数据对齐。

更多详情,请参阅客户端导航指南中的服务器状态和上下文部分。

HTML

保持一致的 HTML 结构

页面之间不一致的 HTML 可能导致模态框丢失其打开/关闭状态、交互元素停止响应,或者生命周期回调(如 data-wp-init 或 data-wp-watch)在应该重新执行时未能重新执行。这是因为 Interactivity API 在底层使用了一种虚拟 DOM 差异算法,该算法会比较当前 HTML 和传入的 HTML 以计算最小的 DOM 变更集。当结构不匹配时,此算法可能会重新挂载元素(导致状态丢失)或错误地重用节点(破坏行为)。

常见问题包括:

  • 不同的元素结构:如果同一个块在一个页面上渲染 <div>,而在另一个页面上渲染 <section>,虚拟 DOM 算法会将它们视为完全不同的元素并替换该节点,从而丢失与其关联的任何状态。
  • 元素获得或失去指令:如果一个元素在一个页面上没有任何 Interactivity API 指令,而在另一个页面上获得了 data-wp-bind 或其他类似指令,差异比较可能无法正确调和它们。
  • 动态兄弟元素:当元素在页面之间出现或消失时(例如条件内容),虚拟 DOM 算法可能难以正确匹配剩余的元素。

为了帮助虚拟 DOM 算法调和元素,请在可能在导航之间发生变化的兄弟元素上使用 data-wp-key 指令。这对于元素列表尤其重要,因为项目可以在页面之间出现、消失或重新排序。如果没有键,虚拟 DOM 算法可能会错误地重用 DOM 节点,导致状态不匹配或视觉故障。

data-wp-key 指令的工作原理类似于 React 中的 key prop —— 它为每个元素赋予差异算法一个稳定的标识符:

<ul>
    <li data-wp-key="item-1">First</li>
    <li data-wp-key="item-2">Second</li>
    <li data-wp-key="item-3">Third</li>
</ul>

使用一个能在导航之间唯一标识每个元素的值,例如文章 ID 或 slug —— 而不是数组索引,因为如果项目重新排序,索引会发生变化。data-wp-key 需要的常见位置包括分页控件、图像画廊以及任何项目在页面之间可能变化的查询驱动列表。

不要在 Interactivity API 之外修改 DOM

直接使用原生 JavaScript API(如 document.createElement()、element.appendChild()、element.remove() 或 jQuery 方法)操作 DOM 与客户端导航不兼容。以此方式添加的元素(如动态创建的提示框或注入的小部件)在离开并返回后将会消失,因为虚拟 DOM 差异并不知晓它们的存在。

始终使用 Interactivity API 指令来修改 DOM 元素。例如,data-wp-text、data-wp-bind、data-wp-each、data-wp-class、data-wp-style 等。

对于需要更新元素的内部 HTML 或使用现有指令无法实现的其他命令式 DOM 更改的情况,请使用 data-wp-watch 指令。此指令会在 响应式状态 每次更改时运行回调,为您提供一种受控的方式来执行副作用 —— 包括命令式 DOM 更新 —— 这些副作用将在每次导航后正确重新执行:

<div
    data-wp-interactive="myPlugin"
    data-wp-watch="callbacks.updateContent"
></div>
import { store } from '@wordpress/interactivity';

store( 'myPlugin', {
    callbacks: {
        updateContent() {
            const { ref } = getElement();
            // Imperative DOM update driven by reactive state.
            ref.innerHTML = state.dynamicContent;
        },
    },
} );

不要在路由器区域之外动态创建 HTML

交互式块应避免将新的 HTML 元素注入到路由器区域之外的 DOM 中 —— 例如,创建附加到 <body> 的覆盖层、模态框或工具提示。

Interactivity API 的客户端导航仅管理 路由器区域 内的内容。在这些区域之外创建的 HTML 在导航期间不会被跟踪、清理或更新。

如果块需要在其主要边界之外渲染内容 —— 例如,必须是 <body> 直接子级的覆盖层 —— 请使用路由器的 attachTo 属性来定义一个可以在导航期间动态创建的区域。这已在 客户端导航指南 中进行了说明。

验证兼容性

在实施上述指南后,使用以下方法来确认客户端导航是否正在与您的块正常工作:

  1. 检查是否有整页重载。 打开浏览器的网络标签(DevTools),然后单击导航到包含您块的页面的链接。如果客户端导航处于活动状态,您将看到针对新页面 HTML 的 fetch 请求,而不是完整的文档导航。完整的文档加载意味着页面上的某些内容不兼容 —— Interactivity API 回退到了传统导航。
  2. 测试交互状态的保留。 打开模态框、展开手风琴或触发块中的任何交互状态。导航到另一个页面然后返回。如果块的状态被保留(或通过 data-wp-init 正确重新初始化),则客户端导航按预期工作。如果块看起来冻结或无响应,请检查 DOM 修改或运行在 Interactivity API 之外的脚本。
  3. 在不同模板之间进行测试。 在使用不同模板的页面之间导航(例如,单篇文章和归档页面)。这将测试 CSS 选择器的稳定性和 HTML 结构的一致性规则,因为不同的模板可能会产生不同的包装元素。

兼容性检查清单

在将您的块标记为与客户端导航兼容之前,请验证以下内容:

  • 该块不会通过 JavaScript 动态注入 <style> 或 <link> 元素。
  • 该块不会在运行时修改现有的样式表(例如,通过 CSSOM API)。
  • CSS 选择器(类名、ID)在导航之间是稳定的 —— 不要对选择器使用 wp_unique_id()。
  • 该块使用脚本模块,而不是普通的 <script> 标签。
  • 该块不从 window.wp.* 全局变量导入 —— 而是使用 ES 模块导入。
  • 该块不依赖 DOMContentLoaded 或 load 事件进行初始化 —— 而是使用 data-wp-init。
  • 如果该块有必须在每次导航时运行的代码,包括刷新当前 URL,它应监视 state.navigating 而不是 state.url。
  • 如果该块需要在每次导航时从服务器同步状态或上下文,它应使用 getServerState() 或 getServerContext()。
  • 可以在导航之间发生变化的兄弟元素列表使用 data-wp-key。
  • 该块不使用 Interactivity API 之外的 API 操作 DOM(例如,document.createElement、jQuery)。
  • 任何需要位于块边界之外的 HTML(例如,位于 <body> 上的覆盖层)都使用 attachTo 来定义其自己的区域。
  • (可选)该块与 实验性整页客户端导航 模式配合良好。