Drupal 11:如何使用 HTMX 创建交互式元素

Drupal 11:使用 HTMX 创建交互式元素

HTMX 是一个 JavaScript 库,借助它,无需编写任何 JavaScript 代码,就能进行 Ajax 调用并创建 CSS 过渡效果。其工作原理是为 HTML 元素添加属性,再利用这些属性设置和执行 Ajax 请求、替换元素以及完成其他操作。

从 Drupal 11.3.0 版本起,HTMX 被集成到了 Drupal 里。这让开发者能够运用渲染数组和 HTML 属性来创建交互式元素。其目标是用基于 HTMX 构建的系统取代整个 Ajax 子系统,不过要达成这一目标,还有不少工作要做。

严格来讲,从 Drupal 11.2.0 版本开始,HTMX 就已存在于 Drupal 中,但当时它只是个实验性库。到了 Drupal 11.3.0 版本,引入了完整的 HTMX 库以及一些辅助类,让 Drupal 开发变得更加轻松。

在本文中,我公司将探讨 HTMX 是如何集成到 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,相比之下,jQuery 库有 30K,而且多年来 Drupal 一直在努力移除对 jQuery 的依赖。HTMX 没有其他依赖项,所以这 16K 就是整个库的大小。

所谓与平台无关,指的是它不关心背后采用的是什么技术,可以有一个由 HTMX 驱动的前端,而后台能够使用几乎任何技术来响应 HTTP 请求。HTMX 会向后台发送普通的 Web 请求,并期望后台以纯 HTML 进行响应。Drupal 在生成 HTML 方面表现出色,因此非常适合作为 HTMX 使用的后端系统。

HTMX 核心具备多种功能,同时它还有一组事件钩子,插件可以利用这些钩子来添加功能和特性。这意味着如果核心 HTMX 库无法满足需求,就可以对其进行扩展,添加一些代码来实现要求。

从根本上说,可以这样理解 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 的功能概述,其他属性会改变这个请求和替换的核心流程的工作方式。

在 Drupal 开发中,只要理解几个关键属性,就能很好地使用 HTMX 和 Drupal,下面我公司来详细了解一下。请注意,这里做了一些简化,但关键概念都有呈现。HTMX.org 的参考页面也有关于这方面的详细文档,大家可以参考该页面获取更多信息。

一、HTTP 动词

通过属性可以使用不同的 HTTP 动词,前面已经看到了 hx - get 属性,此外还有其他几种。

  • hx - get 执行 "get" 请求。
  • hx - post 执行 "post" 请求。
  • hx - delete 执行 "delete" 请求。
  • hx - patch 执行 "patch" 请求。
  • hx - put 执行 "put" 请求。

这允许使用 HTML 通常不使用的 HTTP 动词(即不是 GET 或 POST)向服务器发送请求。

二、hx - target

该属性指定要替换的目标元素,使用 CSS 选择器来定义。

<button hx - get="index.php" hx - target="#div1">提交</button>
<div id="div1"></div>

三、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>

触发器可以添加延迟,以稍微偏移事件触发的请求。以下代码将在元素加载到页面时触发对 index.php 的请求,但会延迟 500 毫秒。

<div hx - post="index.php" hx - trigger="load delay:500ms"></div>

HTMX 还支持 “去抖动” 概念。在以下示例中,将按键抬起触发器延迟 1 秒,如果用户在这段时间内按下另一个键,则之前的事件将被取消,并创建一个新的事件。这意味着不会触发大量事件,只会触发序列中的最后一个事件。

<input hx - get="index.php" hx - trigger="keyup delay:1s" />

四、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 属性从响应中挑选出感兴趣的元素。

五、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" 的元素将从请求中被挑选出来,并用于替换页面中相同的元素。默认情况下,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>

六、hx - swap

该属性控制元素如何被交换到页面中。默认情况下,它是 innerHTML,这将用响应目标替换元素的内容。将其更改为 outerHTML 将替换整个元素。还可以将其设置为 textContent,这将替换内容而不将其解析为 HTML。

其他选项包括:

  • beforebeginafterbeginbeforeendafterend:将响应放置在元素及其子元素之前或之后。
  • delete:从页面中删除目标元素。
  • none:对响应不做任何处理(但仍处理带外项)。

七、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 - * 样式。

一、Drupal 库

通过一个或两个 Drupal 库将 HTMX 包含到页面中。这些库如下:

  • core/htmx:核心 HTMX 库。
  • core/drupal.htmx:核心 HTMX 库以及一些用于与 Drupal 集成的额外脚本。

需要注意的是,很少需要直接注入这些库。当在 Drupal 中与 HTMX 交互时,它会自动为你将这些库注入到页面请求中。

core/drupal.htmx 库包含三个文件,在 Drupal 使用 HTMX 时提供帮助。

  • htmx - assets.js:该文件会查看 Drupal 的响应,并从接收到的标记中提取任何 CSS 和 JavaScript 引用。然后将这些资源注入到当前页面的头部,这意味着任何 CSS 或 JavaScript 都将成为当前页面的一部分,并用于渲染响应。这非常强大,因为这意味着可以在后端渲染任何需要的内容,然后确保它们在前端正确显示。
  • htmx - behaviors.js:该文件连接到 Drupal.behaviors 系统,这样 HTMX 所做的任何 DOM 更改都可以在 Drupal 行为中注册,并用于运行事件。通过添加这个文件,可以确保 HTMX 元素与 Drupal 页面上现有的组件一起工作。
  • htmx - utils.js:该文件包含一些为其他两个文件提供帮助的函数。

为了配合这些库,在后端还有一些 PHP 类。在 Drupal 模块开发中,这些类有着重要作用。

二、Drupal 类

\Drupal\Core\Htmx 类用作 HTMX 库的包装器,并为渲染数组注入属性。它还可以注入用于响应 HTMX 的头部信息。要使用它,只需创建一个新的 Htmx 对象,告诉它希望元素具有哪些 HTMX 属性,然后将其应用到一个渲染元素上。

例如,下面是一个简单的示例,获取一个渲染数组元素,并将 data - hx - swapdata - hx - trigger 属性应用到该元素上。

$output['element'] = [
  '#type' => 'html_tag',
  '#tag' => 'p',
  '#value' => '内容'
];

$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 会发出一些不同的头部信息,可以在代码中监听这些信息,以获取更多关于请求的信息。这可以告诉你诸如是哪个元素触发了请求之类的事情。

三、HTMX Drupal 响应

Drupal 能够使用一个包含你创建的标记的小 HTML 文档来响应 HTMX 请求,这使用了 \Drupal\Core\Render\MainContent\HtmxRenderer 类。

可以直接使用 HtmxRenderer 服务进行响应,但通常通过几种不同的机制来调用它会更简单。可以是在被调用的 URL 中添加查询参数 _wrapper_format=drupal_htmx,或者使用一个在路由参数中添加了 _htmx_route=true 选项的路由。

在设置请求时,可以像下面这样将 _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 页面,包括头部和主体元素。这意味着可能需要从页面请求中挑选出想要更新的元素,在处理表单时尤其如此。 Drupal 升级过程中,这种响应特性也需要关注。

选择哪种方式取决于正在做的事情。下面分别分析一下在控制器和表单中使用 HTMX 的情况,以展示其中的差异。

四、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 
注:由于原文最后一段 PHP 代码不完整,优化后保留原样。若你有更完整的原文或还有其他需求,欢迎随时告诉我。