Drupal 11:使用 HTMX 创建交互元素
HTMX 是一个 JavaScript 库,它能让你在不编写任何 JavaScript 代码的情况下进行 Ajax 调用和创建 CSS 过渡效果。其工作原理是给 HTML 元素添加属性,然后利用这些属性设置和执行 Ajax 请求、替换元素等操作。
在 Drupal 11.3.0 版本中,HTMX 被正式引入,这使得开发者能够使用渲染数组和 HTML 属性创建交互元素。虽然目标是用基于 HTMX 构建的系统取代整个 Ajax 子系统,但要完成这项任务还有很长的路要走。
实际上,从 Drupal 11.2.0 版本起,HTMX 就已存在于 Drupal 中,不过当时仅作为一个实验性库。到了 Drupal 11.3.0 版本,采用了完整的 HTMX 库和一些辅助类,使用起来更加便捷。
在本文中,我将探讨 HTMX 是如何集成到 Drupal 中的,以及 Drupal 系统中有哪些服务可以帮助你使用它。同时,也会涉及到 Drupal 开发、Drupal 模块开发等相关内容,让你对整个 Drupal 生态有更深入的了解。
由于本文篇幅较长,我制作了一个目录,方便你滚动到相关章节。
- HTMX 简介
- HTTP 动词
- hx - target
- hx - trigger
- hx - select
- hx - select - oob
- hx - swap
- hx - swap - oob
- Drupal 中的 HTMX
- Drupal 库
- Drupal 类
- HTMX Drupal 响应
- 控制器中使用 HTMX
- 表单中使用 HTMX
- Drupal 需要你!
- HTMX 模块
- 参考资源
首先,让我们快速了解一下 HTMX。
一、HTMX 简介
HTMX 是一个跨平台的框架,借助它,你无需编写 JavaScript 代码就能进行 Ajax 调用和实现 CSS 过渡效果。
HTMX 库非常小巧,仅有 16K,相比 Drupal 多年来一直试图移除的 30K 的 jQuery 库,优势明显。而且 HTMX 没有其他依赖项,这 16K 就是整个库的大小。
所谓跨平台,是指它不关心后端使用的是什么技术,你可以有一个由 HTMX 驱动的前端,而后端可以使用几乎任何技术来响应 HTTP 请求。HTMX 会向后端发送普通的 Web 请求,并期望后端以纯 HTML 形式响应。Drupal 非常擅长生成 HTML,因此非常适合作为 HTMX 的后端系统,在 Drupal 开发中运用 HTMX 能带来很多便利。
HTMX 核心具备多种功能,同时它还有一组事件钩子,插件可以利用这些钩子添加额外的功能。这意味着如果核心 HTMX 库不能满足你的需求,你可以扩展它并添加一些代码来实现你的需求,在 Drupal 模块开发中也能很好地发挥其扩展性。
从根本上说,你可以这样理解 HTMX:
- 使用属性为 HTML 添加行为。
- 任何元素都可以发起 Web 请求。
- 所有响应都应为 HTML 格式。
HTMX 属性以 hx - * 或 data - hx - * 为前缀,两种用法均可。有多种不同的属性可供使用,但常用的大约只有 11 个,另外还有 20 - 30 个属性可提供更多功能。
例如,让我们看一段与 Drupal 无关的 HTML 代码,其中包含几个 HTMX 属性。
<button hx-get="index.php" hx-target="#div1">提交</button>
<div id="div1"></div>
上述示例中的属性将执行以下操作。
hx - get= 向 index.php 路径发送 GET 请求。hx - target= 将响应内容写入 ID 为 "div1" 的元素中。
此请求的响应应该是纯 HTML 格式,这意味着我们可以用类似以下的内容进行响应。
<p>按钮已点击!</p>
响应内容将像这样注入到我们的 div 元素中。
<div id="div1"><p>按钮已点击!</p></div>
这基本上就是 HTMX 的功能概述,其他属性会改变这个核心的请求和替换过程的工作方式。
你可以在我创建的 HTMX 与 PHP 示例仓库中查看这个示例(以及更多示例)。该仓库包含一个 HTMX PHP 类,它封装了对 HTMX 请求的检测和响应,使示例更易于理解。
在 Drupal 中,只需了解几个关键属性,你就可以很好地使用 HTMX 和 Drupal,下面我们来详细了解一下。请注意,这里我做了一些简化,但关键概念都已涵盖。HTMX.org 参考页面也有关于此的详细文档,如需更多信息请参考该页面。
1. HTTP 动词
通过属性可以使用不同的 HTTP 动词,我们已经看到了 hx - get 属性,此外还有其他一些属性。
hx - get执行 "get" 请求。hx - post执行 "post" 请求。hx - delete执行 "delete" 请求。hx - patch执行 "patch" 请求。hx - put执行 "put" 请求。
这允许你使用 HTML 通常不使用的 HTTP 动词(即非 GET 或 POST)向服务器发送请求。
2. hx - target
该属性指定要替换的目标元素,通过 CSS 选择器来定义。
<button hx-get="index.php" hx-target="#div1">提交</button>
<div id="div1"></div>
3. hx - trigger
定义触发请求的动作。对于具有默认触发动作的元素,此属性可以省略。链接和按钮已经有 "click" 事件,如果为这些元素配置了 HTMX,它们会将该事件传递给 HTMX。其他表单元素(如 select 和 input 元素)会生成 "change" 事件。
以下是几种不同方式使用触发元素的示例。
为 div 元素添加点击触发动作。
<div hx-post="index.php" hx-trigger="click">点击</div>
请注意,这不是最佳实践,为了使点击操作尽可能具有可访问性,你可能还应该包含 ARIA 属性。这里的示例主要关注 HTMX,但 HTML 不应孤立存在。
触发点击事件,并使用 once 进行增强,仅允许该事件执行一次。这意味着用户点击后,按钮不会再发出另一个 Ajax 请求。
<button hx-get="index.php" hx-trigger="click once">点击</button>
为单个 div 标签添加加载和点击触发动作。这意味着当该元素加载到页面上时以及用户点击它时,都可以触发加载内容的操作。
<div hx-post="index.php" hx-trigger="load, click">加载 & 点击</div>
revealed 触发动作直到元素在浏览器窗口的视口中可见时才会触发事件。
<div hx-post="index.php" hx-trigger="revealed"></div>
触发动作可以添加延迟,以稍微偏移事件触发的请求。以下代码将在元素加载到页面上 500 毫秒后触发对 index.php 的请求。
<div hx-post="index.php" hx-trigger="load delay:500ms"></div>
HTMX 也支持 “去抖” 概念。在以下示例中,我们将按键抬起触发动作延迟 1 秒,如果用户在这段时间内按下另一个键,则之前的事件将被取消并创建一个新事件。这意味着我们不会触发大量事件,只会触发序列中的最后一个事件。
<input hx-get="index.php" hx-trigger="keyup delay:1s" />
4. hx - select
此属性使用 CSS 选择器从响应中选择要与目标元素进行替换的内容。
看下面的示例。这里我们有一个 hx - target 属性,它告诉我们结果将放置的位置,同时还有一个 hx - select 属性。该属性用于从响应中选择我们要注入到目标元素中的元素。
<button hx-get="index.php" hx-select="#response" hx-target="#div1">点击</button>
<div id="div1"></div>
假设我们从服务器得到以下响应。
<p id="response">按钮已点击!</p>
<p>一些不会显示的额外内容。</p>
ID 为 "response" 的元素将从响应中被挑选出来并用作内容,其他内容将被忽略。
这个属性在 Drupal 中尤为重要。本文后面会详细解释,当 Drupal 响应 HTMX 请求时,它会返回一整页的 HTML,这意味着我们需要使用 hx - select 属性从响应中挑选出我们感兴趣的元素。
5. hx - select - oob
这里的 “oob” 指的是 “带外” 概念,意味着我们将执行与用户当前操作不一定相关的动作。此属性允许我们对响应执行两个操作。
看下面的示例。
<button hx-get="index.php" hx-select-oob="#div1">发送请求</button>
<div id="div1"></div>
首先,我们将触发对 index.php 脚本的 GET 请求,该请求可能返回如下内容。
请求已发送
<div id="div1">按钮已点击!</div>
按钮的文本将被替换为 “请求已发送”,这是请求的第一部分内容。然后,ID 为 "div1" 的元素将从请求中被挑选出来,用于替换页面中相同 ID 的元素。默认情况下,HTMX 会使用 innerHTML 策略 “替换” 元素。需要记住的是,hx - select - oob 的默认行为是 outerHTML,这意味着整个元素将被替换。
hx - select - oob 属性可以接受以逗号分隔的元素列表,因此以下示例将更改响应中的两个元素,前提是它们在两个位置都存在。
<button hx-get="index.php" hx-select-oob="#div1,#div2">发送请求</button>
<div id="div1"></div> <div id="div2"></div>
6. hx - swap
此属性控制元素如何替换到页面中。默认值是 innerHTML,它将用响应目标替换元素的内容。将其更改为 outerHTML 会替换整个元素。你也可以将其设置为 textContent,这将替换内容而不将其解析为 HTML。
其他选项包括:
beforebegin、afterbegin、beforeend、afterend- 将响应放置在元素及其子元素之前或之后。delete- 从页面中删除目标元素。none- 不处理响应(但仍处理带外项)。
7. hx - swap - oob
替换 “带外” 元素。类似于 hx - select - oob,但响应会告诉 HTMX 将什么内容替换到页面中。
看下面的示例。注意,我们将 hx - swap 属性设置为 "none",这意味着我们不会对该元素进行任何替换操作。
<button hx-get="index.php" hx-swap="none">发送请求</button>
<div id="div1"></div>
服务器的响应包含一个 ID 为 "div1" 的 div 元素,并且 hx - swap - oob 属性设置为 true。
<div id="div1" hx-swap-oob="true">按钮已点击!</div>
这将在页面上找到具有相同 ID 的元素并将其替换为该元素。这实际上使我们能够从服务器控制页面上的元素,功能非常强大。
二、Drupal 中的 HTMX
HTMX 支持使用 hx - * 和 data - hx - * 属性。Drupal 默认选择使用 data - hx - * 属性。如果你愿意,仍然可以使用 hx - * 属性,但 Drupal 在渲染 HTMX 时默认会输出 data - hx - * 样式的属性。
1. Drupal 库
通过一个或两个 Drupal 库将 HTMX 包含到页面中,这些库如下:
core/htmx- 核心 HTMX 库。core/drupal.htmx- 核心 HTMX 库以及一些用于与 Drupal 集成的附加脚本。
需要注意的是,你很少需要直接注入这些库。当你在 Drupal 中使用 HTMX 时,它会自动为你将这些库注入到页面请求中。
core/drupal.htmx 库包含三个文件,在 Drupal 使用 HTMX 时提供帮助。这些文件在 Drupal 开发过程中起到了重要作用。
htmx - assets.js- 该文件会查看 Drupal 的响应,并从接收到的标记中提取任何 CSS 和 JavaScript 引用。然后将这些资源注入到当前页面的头部,这意味着任何 CSS 或 JavaScript 都将成为当前页面的一部分,并用于渲染响应内容。这非常强大,因为这意味着我们可以在后端渲染任何所需内容,并且可以确保它们在前端正确显示。htmx - behaviors.js- 该文件连接到Drupal.behaviors系统,以便 HTMX 所做的任何 DOM 更改都可以在 Drupal 行为中注册并用于运行事件。通过添加这个文件,我们可以确保 HTMX 元素与 Drupal 页面上现有的组件协同工作。htmx - utils.js- 该文件包含一些为其他两个文件提供帮助的函数。
为了配合这些库,我们在后端还有一些 PHP 类。
2. Drupal 类
\Drupal\Core\Htmx 类用作 HTMX 库和渲染数组属性注入的包装器。它还可以注入用于响应 HTMX 的头部信息。要使用它,你只需创建一个新的 Htmx 对象,告诉它你希望在元素中使用哪些 HTMX 属性,然后将其应用到渲染元素上。
例如,下面是一个简单的示例,我们获取一个渲染数组元素,并将 data - hx - swap 和 data - hx - trigger 属性应用到该元素上。
$output['element'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => 'Content'
];
$htmx = new Htmx();
$htmx->get()
->swap('afterend')
->trigger('revealed')
->applyTo($output['element']);
渲染时,这将生成以下 HTML。
<p data-hx-get="/route" data-hx-swap="afterend" data-hx-trigger="revealed">内容</p>
你可以看到这些方法对应于 HTMX 库中的等效属性,所以 trigger() 方法会创建一个 data - hx - trigger 属性,以此类推。
我们调用 get() 方法来设置一个 GET 请求。这个方法可以接受一个参数,即执行 GET 请求时应使用的路由。如果我们省略这个参数(就像这里一样),则默认使用当前路由。
通过使用这种机制,我们会自动将 core/drupal.htmx 库包含在页面附件中。这意味着如果你在页面中添加一个 HTMX 元素,那么前端就会自动获得渲染和正确使用该元素所需的一切。
你可能还会看到类似这样的语法,本质上是一样的。这种语法叫做 “实例化时的类成员访问”,自 PHP 5.4 版本起就存在了,不过不太常见。
(new Htmx())
->get()
->swap('afterend')
->trigger('revealed')
->applyTo($output['element']);
除了 Htmx 类,我们还有一个 \Drupal\Core\Htmx\HtmxRequestInfoTrait 特性,用于检测 HTMX 请求和相关头部信息。这个特性需要访问 request_stack 服务和一个名为 getRequest() 的方法,以便从当前请求中提取信息。
这里我没有详细说明,但 HTMX 会发送许多不同的头部信息,你可以在代码中监听这些信息,以获取更多关于请求的信息,比如是什么元素触发了请求。
3. HTMX Drupal 响应
Drupal 能够使用类 \Drupal\Core\Render\MainContent\HtmxRenderer 以包含你创建的标记的小 HTML 文档来响应 HTMX 请求。
你 可以 直接使用 HtmxRenderer 服务进行响应,但通常通过几种不同的机制调用它会更方便。可以是在被调用的 URL 中添加查询参数 _wrapper_format=drupal_htmx,或者使用在路由参数中添加了 _htmx_route=true 选项的路由。在 Drupal 升级后,这些机制可能会有一些优化,但核心的使用方式还是万变不离其宗。
在设置请求时,你可以像下面这样将 _wrapper_format 添加到路由中。
$htmx->get(Url::fromRoute(route_name: 'my_htmx_route', options: [
'query' => [
'_wrapper_format' => 'drupal_htmx',
],
]));
在响应请求的路由中添加 _htmx_route=true。
mymodule_controller_action_htmx:
path: '/htmx-response'
defaults:
_controller: '\Drupal\mymodule\Controller\MyModuleController::htmx'
requirements:
_permission: 'access content'
options:
_htmx_route: TRUE
Drupal 响应的重要一点是,它是一 整页的 HTML,包括头部和主体元素。这意味着你可能需要从页面请求中挑选出你想要更新的元素,在处理表单时尤其如此。
这里选择哪种方式取决于你的具体操作。下面我们分别分析在控制器和表单中使用 HTMX 的差异。
4. 控制器中使用 HTMX
要将 HTMX 集成到控制器中,你需要考虑采用的方法。你可以选择:
- 使用一个路由:在该路由中编写逻辑来分离出 HTMX 响应。
- 使用两个路由:一个用于渲染页面,另一个标记为
_htmx_route: TRUE用于处理 HTMX 响应。
如果你使用单个路由,那么需要在控制器中使用 HtmxRequestInfoTrait 来检测传入的请求是否为 HTMX 请求。
要使用 HtmxRequestInfoTrait 特性,你需要添加请求栈服务并包含一个名为 getRequest() 的方法。以下是一个模板,它包含了你所需的一切。
namespace Drupal\mymodule\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Htmx\HtmxRequestInfoTrait;
class MyController extends ControllerBase {
use HtmxRequestInfoTrait;
public function __construct(protected RequestStack $requestStack) {}
protected function getRequest() {
return $this->requestStack->getCurrentRequest();
}
...
做好这些准备后,你可以在操作中进行如下操作。
public function action() {
if ($this->isHtmxRequest())

