文档

htmx简介

htmx是一个库,允许您直接从HTML访问现代浏览器功能,而无需使用JavaScript。

要理解htmx,首先让我们看一个锚标签:

<a href="/blog">博客</a>

这个锚标签告诉浏览器:

"当用户点击此链接时,向'/blog'发出HTTP GET请求,并将响应内容加载到浏览器窗口中"。

考虑到这一点,请看以下HTML代码:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    点击我!
</button>

这告诉htmx:

"当用户点击此按钮时,向'/clicked'发出HTTP POST请求,并使用响应内容替换DOM中id为parent-div的元素"

htmx扩展并概括了HTML作为超文本的核心思想,直接在语言中开启了更多可能性:

  • 现在任何元素,不仅仅是锚和表单,都可以发出HTTP请求
  • 现在任何事件,不仅仅是点击或表单提交,都可以触发请求
  • 现在任何HTTP动词,不仅仅是GETPOST,都可以使用
  • 现在任何元素,不仅仅是整个窗口,都可以作为请求更新的目标

请注意,当您使用htmx时,在服务器端通常响应的是HTML,而不是JSON。这使您完全遵循原始Web编程模型,使用超文本作为应用程序状态的引擎,甚至不需要真正理解这个概念。

值得一提的是,如果您愿意,可以在使用htmx时使用data-前缀:

<a data-hx-post="/click">点击我!</a>

如果您理解了htmx的概念并想了解库的特性,请参阅我们的特性页面。

1.x到2.x迁移指南

htmx 1.x版本仍然支持IE11,但最新版本是2.x。

如果您从htmx 1.x迁移到htmx 2.x,请参阅htmx 1.x迁移指南

如果您从intercooler.js迁移到htmx,请参阅intercooler迁移指南

安装

Htmx是一个无依赖、面向浏览器的JavaScript库。这意味着使用它就像在文档头部添加一个<script>标签一样简单。不需要构建系统来使用它。

通过CDN(如unpkg)

使用htmx最快的方法是通过CDN加载。您只需将此添加到您的head标签中即可开始:

<script src="https://unpkg.com/htmx.org@2.0.6/dist/htmx.min.js" ></script>

也有未压缩版本可用:

<script src="https://unpkg.com/htmx.org@2.0.6/dist/htmx.js" ></script>

虽然CDN方法非常简单,但您可能需要考虑不在生产环境中使用CDN

下载副本

安装htmx的下一个最简单方法是将其复制到您的项目中。

unpkg下载htmx.min.js,并将其添加到项目中的适当目录,然后在需要的地方使用<script>标签包含它:

<script src="/path/to/htmx.min.js"></script>

npm

对于npm风格的构建系统,您可以通过npm安装htmx:

npm install htmx.org@2.0.6

安装后,您需要使用适当的工具来使用node_modules/htmx.org/dist/htmx.js(或.min.js)。例如,您可以将htmx与一些扩展和项目特定代码捆绑在一起。

Webpack

如果您使用webpack管理JavaScript:

  • 通过您喜欢的包管理器(如npm或yarn)安装htmx
  • 将导入添加到您的index.js
import 'htmx.org';

如果您想使用全局htmx变量(推荐),您需要将其注入到window作用域:

  • 创建一个自定义JS文件
  • 将此文件导入到您的index.js(在步骤2的导入下方)
import 'path/to/my_custom.js';
  • 然后将此代码添加到文件中:
window.htmx = require('htmx.org');
  • 最后,重新构建您的包

AJAX

htmx的核心是一组属性,允许您直接从HTML发出AJAX请求:

属性描述
hx-get向给定URL发出GET请求
hx-post向给定URL发出POST请求
hx-put向给定URL发出PUT请求
hx-patch向给定URL发出PATCH请求
hx-delete向给定URL发出DELETE请求

这些属性中的每一个都接受一个URL来发出AJAX请求。当元素被触发时,它将向指定的URL发出指定类型的请求:

<button hx-put="/messages">
    发送到消息
</button>

这告诉浏览器:

当用户点击此按钮时,向URL /messages发出PUT请求,并将响应加载到按钮中

触发请求

默认情况下,AJAX请求由元素的"自然"事件触发:

  • inputtextareaselectchange事件上触发
  • formsubmit事件上触发
  • 其他所有元素在click事件上触发

如果您想要不同的行为,可以使用hx-trigger属性指定哪个事件将导致请求。

这是一个div,当鼠标进入时会向/mouse_entered发送POST请求:

<div hx-post="/mouse_entered" hx-trigger="mouseenter">
    [鼠标,来这里!]
</div>

触发器修饰符

触发器还可以有一些额外的修饰符来改变其行为。例如,如果您希望请求只发生一次,可以使用触发器的once修饰符:

<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
    [鼠标,来这里!]
</div>

您可以用于触发器的其他修饰符有:

  • changed - 仅当元素的值已更改时才发出请求
  • delay:<时间间隔> - 在发出请求前等待给定的时间(例如1s)。如果事件再次触发,倒计时将重置。
  • throttle:<时间间隔> - 在发出请求前等待给定的时间(例如1s)。与delay不同,如果在时间限制内发生新事件,则该事件将被丢弃,因此请求将在时间段的末尾触发。
  • from:<CSS选择器> - 在不同的元素上监听事件。这可以用于键盘快捷键等。请注意,如果页面发生变化,此CSS选择器不会重新评估。

您可以使用这些属性来实现许多常见的UX模式,例如主动搜索

<input type="text" name="q"
    hx-get="/trigger_delay"
    hx-trigger="keyup changed delay:500ms"
    hx-target="#search-results"
    placeholder="搜索...">
<div id="search-results"></div>

此输入将在按键事件后500毫秒发出请求(如果输入已更改),并将结果加载到id为search-resultsdiv中。

可以在hx-trigger属性中指定多个触发器,用逗号分隔。

触发器过滤器

您还可以通过在事件名称后使用方括号来应用触发器过滤器,其中包含一个将被评估的JavaScript表达式。如果表达式评估为true,则事件将触发,否则不会。

这是一个仅在元素上Control-Click时触发的示例

<div hx-get="/clicked" hx-trigger="click[ctrlKey]">
    Control点击我
</div>

ctrlKey这样的属性将首先针对触发事件解析,然后针对全局作用域。this符号将设置为当前元素。

特殊事件

htmx在hx-trigger中提供了一些特殊事件:

  • load - 在元素首次加载时触发一次
  • revealed - 在元素首次滚动到视口中时触发一次
  • intersect - 在元素首次与视口相交时触发一次。这支持两个附加选项:
    • root:<选择器> - 用于相交的根元素的CSS选择器
    • threshold:<浮点数> - 介于0.0和1.0之间的浮点数,指示触发事件所需的相交量

如果您有高级用例,还可以使用自定义事件来触发请求。

轮询

如果您希望元素轮询给定的URL而不是等待事件,可以在hx-trigger属性中使用every语法:

<div hx-get="/news" hx-trigger="every 2s"></div>

这告诉htmx

每2秒向/news发出GET请求,并将响应加载到div中

如果您想从服务器响应中停止轮询,可以响应HTTP响应代码286,元素将取消轮询。

加载轮询

htmx中实现轮询的另一种技术是"加载轮询",其中元素指定load触发器以及延迟,并用响应替换自身:

<div hx-get="/messages"
    hx-trigger="load delay:1s"
    hx-swap="outerHTML">
</div>

如果/messages端点继续返回这样设置的div,它将每秒轮询URL一次。

加载轮询在轮询有端点的情况下非常有用,此时轮询终止,例如当您向用户显示进度条时。

请求指示器

当发出AJAX请求时,通常最好让用户知道正在发生某些事情,因为浏览器不会给他们任何反馈。您可以通过使用htmx-indicator类在htmx中实现这一点。

htmx-indicator类的定义使得任何具有此类的元素的默认不透明度为0,使其不可见但存在于DOM中。

当htmx发出请求时,它会在元素(请求元素或指定的另一个元素)上放置一个htmx-request类。htmx-request类将使具有htmx-indicator类的子元素过渡到不透明度1,显示指示器。

<button hx-get="/click">
    点击我!
    <img class="htmx-indicator" src="/spinner.gif">
</button>

这里我们有一个按钮。当它被点击时,htmx-request类将被添加到它,这将显示旋转的gif元素。(我最近喜欢SVG旋转器。)

虽然htmx-indicator类使用不透明度来隐藏和显示进度指示器,但如果您更喜欢其他机制,可以创建自己的CSS过渡,如下所示:

.htmx-indicator{
    display:none;
}
.htmx-request .htmx-indicator{
    display:inline;
}
.htmx-request.htmx-indicator{
    display:inline;
}

如果您希望htmx-request类添加到不同的元素,可以使用hx-indicator属性与CSS选择器来实现:

<div>
    <button hx-get="/click" hx-indicator="#indicator">
        点击我!
    </button>
    <img id="indicator" class="htmx-indicator" src="/spinner.gif"/>
</div>

这里我们通过id明确指出了指示器。请注意,我们也可以将类放在父div上,效果相同。

您还可以通过使用hx-disabled-elt属性在请求期间向元素添加disabled属性

目标

如果您希望响应加载到与发出请求的元素不同的元素中,可以使用hx-target属性,该属性接受CSS选择器。回顾我们的实时搜索示例:

<input type="text" name="q"
    hx-get="/trigger_delay"
    hx-trigger="keyup delay:500ms changed"
    hx-target="#search-results"
    placeholder="搜索...">
<div id="search-results"></div>

您可以看到搜索结果将被加载到div#search-results中,而不是输入标签中。

扩展CSS选择器

hx-target和大多数接受CSS选择器的属性支持"扩展"CSS语法:

  • 您可以使用this关键字,表示hx-target属性所在的元素是目标
  • closest <CSS选择器>语法将找到与给定CSS选择器匹配的最近祖先元素或自身(例如closest tr将找到元素最近的表格行)
  • next <CSS选择器>语法将找到DOM中与给定CSS选择器匹配的下一个元素
  • previous <CSS选择器>语法将找到DOM中与给定CSS选择器匹配的上一个元素
  • find <CSS选择器>将找到与给定CSS选择器匹配的第一个子后代元素(例如find tr将定位元素的第一个子后代行)

此外,CSS选择器可以用</>字符包裹,模仿hyperscript的查询字面量语法。

像这样的相对目标对于创建灵活的用户界面非常有用,而无需在DOM中散布大量id属性。

交换

htmx提供了几种不同的方式来将返回的HTML交换到DOM中。默认情况下,内容替换目标元素的innerHTML。您可以通过使用hx-swap属性与以下任何值来修改此行为:

名称描述
innerHTML默认值,将内容放在目标元素内部
outerHTML用返回的内容替换整个目标元素
afterbegin在目标内部第一个子元素之前前置内容
beforebegin在目标的父元素中目标之前前置内容
beforeend在目标内部最后一个子元素之后追加内容
afterend在目标的父元素中目标之后追加内容
delete无论响应如何都删除目标元素
none不追加响应中的内容(仍会处理带外交换响应头

变形交换

除了上述标准交换机制外,htmx还通过扩展支持变形交换。变形交换尝试将新内容合并到现有DOM中,而不是简单地替换它。它们通常在交换操作期间通过就地改变现有节点来更好地保留焦点、视频状态等内容,但代价是更多的CPU。

以下扩展可用于变形风格的交换:

视图过渡

新的实验性视图过渡API为开发者提供了一种在不同DOM状态之间创建动画过渡的方式。它仍在积极开发中,并非所有浏览器都支持,但htmx提供了一种与此新API配合使用的方法,如果API在给定浏览器中不可用,则回退到非过渡机制。

您可以使用以下方法尝试此新API:

  • htmx.config.globalViewTransitions配置变量设置为true以对所有交换使用过渡
  • hx-swap属性中使用transition:true选项
  • 如果由于上述任一配置元素交换将过渡,您可以捕获htmx:beforeTransition事件并对其调用preventDefault()以取消过渡。

视图过渡可以使用CSS进行配置,如Chrome功能文档中所述。

您可以在动画示例页面上看到视图过渡示例。

交换选项

hx-swap属性支持许多选项来调整htmx的交换行为。例如,默认情况下htmx会交换新内容中任何地方找到的title标签的标题。您可以通过将ignoreTitle修饰符设置为true来关闭此行为:

    <button hx-post="/like" hx-swap="outerHTML ignoreTitle:true">点赞</button>

hx-swap上可用的修饰符有:

选项描述
transitiontruefalse,是否为此交换使用视图过渡API
swap使用的交换延迟(例如100ms),在旧内容清除和新内容插入之间
settle使用的稳定延迟(例如100ms),在新内容插入和稳定之间
ignoreTitle如果设置为true,将忽略新内容中找到的任何标题,并且不更新文档标题
scrolltopbottom,将目标元素滚动到其顶部或底部
showtopbottom,将目标元素的顶部或底部滚动到视图中

所有交换修饰符在指定交换样式后出现,并用冒号分隔。

有关这些选项的更多详细信息,请参阅hx-swap文档。

同步

通常您希望协调两个元素之间的请求。例如,您可能希望一个元素的请求取代另一个元素的请求,或者等到另一个元素的请求完成。

htmx提供了一个hx-sync属性来帮助您实现这一点。

考虑以下HTML中表单提交和单个输入验证请求之间的竞争条件:

<form hx-post="/store">
    <input id="title" name="title" type="text"
        hx-post="/validate"
        hx-trigger="change">
    <button type="submit">提交</button>
</form>

不使用hx-sync,填写输入并立即提交表单会触发两个并行请求到/validate/store

在输入上使用hx-sync="closest form:abort"将监视表单上的请求,并在表单请求存在或在输入请求进行中开始时中止输入的请求:

<form hx-post="/store">
    <input id="title" name="title" type="text"
        hx-post="/validate"
        hx-trigger="change"
        hx-sync="closest form:abort">
    <button type="submit">提交</button>
</form>

这以声明方式解决了两个元素之间的同步问题。

htmx还支持以编程方式取消请求:您可以向元素发送htmx:abort事件以取消任何进行中的请求:

<button id="request-button" hx-post="/example">
    发出请求
</button>
<button onclick="htmx.trigger('#request-button', 'htmx:abort')">
    取消请求
</button>

更多示例和详细信息可以在hx-sync属性页面上找到。

CSS过渡

htmx使得无需JavaScript即可轻松使用CSS过渡。考虑以下HTML内容:

<div id="div1">原始内容</div>

想象一下,此内容通过ajax请求被htmx替换为以下新内容:

<div id="div1" class="red">新内容</div>

注意两点:

  • div元素在原始内容和新内容中具有相同的id
  • 新内容中添加了red

在这种情况下,我们可以编写一个从旧状态到新状态的CSS过渡效果:

.red {
    color: red;
    transition: all ease-in 1s ;
}

当htmx交换新内容时,它会以CSS过渡效果应用到新内容的方式执行,为您提供一个平滑过渡到新状态的体验。

总结来说,要为元素使用CSS过渡效果,只需在请求间保持其id稳定!

您可以在动画示例中查看更多详情和实时演示。

详情

要理解CSS过渡在htmx中的工作原理,您需要了解htmx使用的底层交换和稳定模型。

当从服务器接收到新内容时,在交换内容之前,会检查页面现有内容中与id属性匹配的元素。如果在新内容中找到匹配的元素,交换发生前会将旧内容的属性复制到新元素上。然后交换新内容,但使用属性值。最后,在"稳定"延迟(默认20毫秒)后交换新的属性值。虽然有点复杂,但这正是让开发者无需JavaScript就能实现CSS过渡效果的机制。

带外交换

如果想直接将响应内容通过id属性交换到DOM中,可以在响应HTML中使用hx-swap-oob属性:

<div id="message" hx-swap-oob="true">直接交换我!</div>
附加内容

在这个响应中,div#message将直接交换到匹配的DOM元素中,而附加内容会以常规方式交换到目标中。

可以使用此技术在其他请求上"捎带"更新。

问题表格

表格元素与带外交换结合使用时可能会有问题,因为根据HTML规范,许多表格元素不能独立存在于DOM中(如<tr><td>)。

为避免此问题,可以使用template标签封装这些元素:

<template>
  <tr id="message" hx-swap-oob="true"><td>Joe</td><td>Smith</td></tr>
</template>

选择要交换的内容

如果想选择响应HTML的子集交换到目标中,可以使用hx-select属性,它接受一个CSS选择器并从响应中选择匹配的元素。

还可以使用hx-select-oob属性挑选内容进行带外交换,它接受要挑选和交换的元素ID列表。

在交换期间保留内容

如果有希望在交换期间保留的内容(例如希望即使发生交换也能继续播放的视频播放器),可以在要保留的元素上使用hx-preserve属性。

参数

默认情况下,触发请求的元素如果具有值则会包含其值。如果元素是表单,则会包含其中所有输入的值。

与HTML表单一样,输入的name属性用作htmx发送请求中的参数名称。

此外,如果元素触发非GET请求,则相关表单的所有输入值都将被包含(通常是最近的封闭表单,但如果使用<button form="associated-form">可能会不同)。

如果想包含其他元素的值,可以使用hx-include属性,并指定要包含值的所有元素的CSS选择器。

如果想过滤某些参数,可以使用hx-params属性。

最后,如果想以编程方式修改参数,可以使用htmx:configRequest事件。

文件上传

如果想通过htmx请求上传文件,可以将hx-encoding属性设置为multipart/form-data。这将使用FormData对象提交请求,从而正确包含文件。

注意:根据服务器端技术,可能需要以非常不同的方式处理这种类型的请求体内容。

注意:htmx在上传过程中会基于标准的progress事件定期触发htmx:xhr:progress事件,可以挂钩此事件以显示上传进度。

有关更高级的表单模式,包括进度条错误处理,请参阅示例部分

额外值

可以使用hx-vals(JSON格式的名称-表达式对)和hx-vars属性(动态计算的逗号分隔名称-表达式对)在请求中包含额外值。

确认请求

通常希望在发出请求前确认操作。htmx支持hx-confirm属性,允许使用简单的JavaScript对话框确认操作:

<button hx-delete="/account" hx-confirm="确定要删除您的账户吗?">
    删除我的账户
</button>

使用事件可以实现更复杂的确认对话框。确认示例展示了如何使用sweetalert2库确认htmx操作。

使用事件确认请求

另一种确认方法是使用htmx:confirm事件。此事件在每个请求的触发器上触发(不仅限于具有hx-confirm属性的元素),可用于实现请求的异步确认。

以下是使用sweet alert在任何具有confirm-with-sweet-alert='true'属性的元素上的示例:

document.body.addEventListener('htmx:confirm', function(evt) {
  if (evt.target.matches("[confirm-with-sweet-alert='true']")) {
    evt.preventDefault();
    swal({
      title: "确定吗?",
      text: "您确定要这样做吗?",
      icon: "warning",
      buttons: true,
      dangerMode: true,
    }).then((confirmed) => {
      if (confirmed) {
        evt.detail.issueRequest();
      }
    });
  }
});

属性继承

htmx中的大多数属性都是可继承的:它们应用于它们所在的元素以及任何子元素。这允许您将属性"提升"到DOM中以避免代码重复。考虑以下htmx代码:

<button hx-delete="/account" hx-confirm="确定吗?">
    删除我的账户
</button>
<button hx-put="/account" hx-confirm="确定吗?">
    更新我的账户
</button>

这里我们有一个重复的hx-confirm属性。我们可以将此属性提升到父元素:

<div hx-confirm="确定吗?">
    <button hx-delete="/account">
        删除我的账户
    </button>
    <button hx-put="/account">
        更新我的账户
    </button>
</div>

现在,这个hx-confirm属性将应用于其中的所有htmx驱动元素。

有时您希望撤销这种继承。假设我们为这组按钮添加了一个取消按钮,但不希望它被确认。我们可以像这样添加一个unset指令:

<div hx-confirm="确定吗?">
    <button hx-delete="/account">
        删除我的账户
    </button>
    <button hx-put="/account">
        更新我的账户
    </button>
    <button hx-confirm="unset" hx-get="/">
        取消
    </button>
</div>

前两个按钮将显示确认对话框,但底部的取消按钮不会。

可以使用hx-disinherit属性按元素和按属性禁用继承。

如果想完全禁用属性继承,可以将htmx.config.disableInheritance配置变量设置为true。这将默认禁用继承,并允许您使用hx-inherit属性显式指定继承。

增强

Htmx支持使用hx-boost属性"增强"常规HTML锚点和表单。此属性将把所有锚点标签和表单转换为AJAX请求,默认情况下以页面主体为目标。

以下是一个示例:

<div hx-boost="true">
    <a href="/blog">博客</a>
</div>

此div中的锚点标签将向/blog发出AJAX GET请求,并将响应交换到body标签中。

渐进增强

hx-boost的一个特性是,如果未启用JavaScript,它会优雅降级:链接和表单仍然可以工作,只是不使用AJAX请求。这被称为渐进增强,它允许更广泛的受众使用您网站的功能。

其他htmx模式也可以适应实现渐进增强,但它们需要更多的思考。

考虑主动搜索示例。按照编写方式,它不会优雅降级:未启用JavaScript的用户将无法使用此功能。这是为了简单起见,使示例尽可能简短。

但是,您可以将htmx增强的输入包装在表单元素中:

<form action="/search" method="POST">
    <input class="form-control" type="search"
        name="search" placeholder="开始输入以搜索用户..."
        hx-post="/search"
        hx-trigger="keyup changed delay:500ms, search"
        hx-target="#search-results"
        hx-indicator=".htmx-indicator">
</form>

这样,启用JavaScript的客户端仍然可以获得良好的主动搜索用户体验,但未启用JavaScript的客户端可以按Enter键仍然可以搜索。更好的是,您还可以添加一个"搜索"按钮。然后需要使用与action属性匹配的hx-post更新表单,或者在其上使用hx-boost

需要在服务器端检查HX-Request头,以区分htmx驱动的请求和常规请求,确定向客户端呈现什么内容。

其他模式可以类似地适应,以满足应用程序的渐进增强需求。

正如您所见,这需要更多的思考和更多的工作。它还完全排除了某些功能。这些权衡必须由您作为开发者根据项目目标和受众做出。

可访问性是与渐进增强密切相关的概念。使用渐进增强技术(如hx-boost)将使您的htmx应用程序对更广泛的用户更具可访问性。

基于htmx的应用程序与普通的非AJAX驱动的Web应用程序非常相似,因为htmx是面向HTML的。

因此,正常的HTML可访问性建议适用。例如:

  • 尽可能使用语义HTML(即正确的事物使用正确的标签)
  • 确保焦点状态清晰可见
  • 为所有表单字段关联文本标签
  • 通过适当的字体、对比度等最大化应用程序的可读性

WebSocket和SSE

WebSocket和服务器发送事件(SSE)通过扩展支持。请参阅SSE扩展WebSocket扩展页面了解更多信息。

历史支持

Htmx提供了一个简单的机制与浏览器历史API交互:

如果希望给定元素将其请求URL推送到浏览器导航栏并将页面的当前状态添加到浏览器的历史记录中,请包含hx-push-url属性:

<a hx-get="/blog" hx-push-url="true">博客</a>

当用户点击此链接时,htmx将在向/blog发出请求之前对当前DOM进行快照并存储。然后执行交换并将新位置推入历史堆栈。

当用户点击后退按钮时,htmx将从存储中检索旧内容并将其交换回目标,模拟"返回"到先前状态。如果在缓存中找不到位置,htmx将向给定URL发出AJAX请求,并设置HX-History-Restore-Request头为true,期望返回整个页面所需的HTML。您应始终将htmx.config.historyRestoreAsHxRequest设置为false以防止HX-Request头,然后可以安全地用于响应部分内容。或者,如果htmx.config.refreshOnHistoryMiss配置变量设置为true,它将执行硬浏览器刷新。

注意:如果将URL推入历史记录,您必须能够导航到该URL并获取完整页面!用户可能会将URL复制并粘贴到电子邮件或新标签页中。此外,如果页面不在历史缓存中,htmx在恢复历史记录时将需要整个页面。

指定历史快照元素

默认情况下,htmx将使用body来拍摄和恢复历史快照。这通常是正确的选择,但如果想使用更窄的元素进行快照,可以使用hx-history-elt属性指定不同的元素。

注意:此元素需要出现在所有页面上,否则历史恢复将无法可靠工作。

撤销第三方库的DOM变更

如果使用第三方库并希望使用htmx历史功能,需要在拍摄快照之前清理DOM。让我们考虑Tom Select库,它使选择元素具有更丰富的用户体验。让我们设置TomSelect将任何具有.tomselect类的输入元素转换为丰富的选择元素。

首先,我们需要在新内容中初始化具有该类的元素:

htmx.onLoad(function (target) {
    // 在新内容中找到所有应该成为编辑器的元素并使用TomSelect初始化
    var editors = target.querySelectorAll(".tomselect")
            .forEach(elt => new TomSelect(elt))
});

这将为所有具有.tomselect类的输入元素创建一个丰富的选择器。然而,它变更了DOM,我们不希望这些变更被保存到历史缓存中,因为当历史内容加载回屏幕时,TomSelect将重新初始化。

为了处理这个问题,我们需要捕获htmx:beforeHistorySave事件,并通过调用destroy()清理TomSelect的变更:

htmx.on('htmx:beforeHistorySave', function() {
    // 找到所有TomSelect元素
    document.querySelectorAll('.tomSelect')
            .forEach(elt => elt.tomselect.destroy()) // 并对它们调用destroy()
})

禁用历史快照

可以通过在当前文档中的任何元素或htmx加载到当前文档的任何HTML片段上将hx-history属性设置为false来禁用URL的历史快照。这可以防止敏感数据进入localStorage缓存,这对于共享/公共计算机非常重要。历史导航将按预期工作,但在恢复时将从服务器请求URL,而不是本地历史缓存。

请求和响应

Htmx期望对其发出的AJAX请求的响应是HTML,通常是HTML片段(尽管完整的HTML文档,与hx-select标签匹配也可能有用)。然后,htmx将返回的HTML交换到文档中指定的目标,并使用指定的交换策略。

有时您可能希望在交换中不做任何操作,但仍然可能触发客户端事件(见下文)。

对于这种情况,默认情况下,您可以返回204 - 无内容响应代码,htmx将忽略响应的内容。

如果服务器返回错误响应(例如404或501),htmx将触发htmx:responseError事件,您可以处理该事件。

如果发生连接错误,将触发htmx:sendError事件。

配置响应处理

可以通过修改或替换htmx.config.responseHandling数组来配置上述htmx行为。此对象是JavaScript对象的集合,定义如下:

    responseHandling: [
        {code:"204", swap: false},   // 204 - 无内容默认不执行任何操作,但不是错误
        {code:"[23]..", swap: true}, // 200和300响应是非错误且被交换
        {code:"[45]..", swap: false, error:true}, // 400和500响应不被交换且是错误
        {code:"...", swap: false}    // 其他任何响应代码的捕获
    ]

当htmx收到响应时,它将按顺序遍历htmx.config.responseHandling数组,并测试给定对象的code属性(视为正则表达式)是否匹配当前响应。如果条目匹配当前响应代码,则将用于确定是否以及如何处理响应。

此数组中条目可用的响应处理配置字段包括:

  • code - 表示将针对响应代码测试的正则表达式的字符串。
  • swap - 如果应交换响应到DOM,则为true,否则为false
  • error - 如果htmx应将此响应视为错误,则为true
  • ignoreTitle - 如果htmx应忽略响应中的title标签,则为true
  • select - 用于从响应中选择内容的CSS选择器
  • target - 指定响应替代目标的CSS选择器
  • swapOverride - 响应的替代交换机制

配置响应处理示例

作为如何使用此配置的示例,考虑当服务器端框架在验证错误发生时响应422 - 不可处理实体响应的情况。默认情况下,htmx将忽略响应,因为它匹配正则表达式[45]..

使用元配置机制配置responseHandling,我们可以添加以下配置:

<!--
  * 204 无内容默认不执行任何操作,但不是错误
  * 2xx、3xx和422响应是非错误且被交换
  * 4xx和5xx响应不被交换且是错误
  * 所有其他响应使用"..."作为捕获全部进行交换
-->
<meta
	name="htmx-config"
	content='{
        "responseHandling":[
            {"code":"204", "swap": false},
            {"code":"[23]..", "swap": true},
            {"code":"422", "swap": true},
            {"code":"[45]..", "swap": false, "error":true},
            {"code":"...", "swap": true}
        ]
    }'
/>

如果想交换所有内容,无论HTTP响应代码如何,可以使用此配置:

<meta name="htmx-config" content='{"responseHandling": [{"code":".*", "swap": true}]}' /> <!--所有响应都被交换-->

最后,值得考虑使用响应目标扩展,它允许您基于HTTP响应代码(例如404)声明性地配置元素的行为。

CORS

在跨源上下文中使用htmx时,请记住配置您的Web服务器设置Access-Control头,以便htmx头在客户端可见。

查看htmx实现的所有请求和响应头。

请求头

htmx在请求中包含许多有用的头:

描述
HX-Boosted表示请求是通过使用hx-boost的元素发出的
HX-Current-URL浏览器的当前URL
HX-History-Restore-Request如果请求是由于本地历史缓存未命中而恢复历史记录,则为"true"
HX-Prompt用户对hx-prompt的响应
HX-Request始终为"true",除非在历史恢复请求上且'htmx.config.historyRestoreAsHxRequest'禁用
HX-Target目标元素的id(如果存在)
HX-Trigger-Name触发元素的name(如果存在)
HX-Trigger触发元素的id(如果存在)

响应头

htmx支持一些特定于htmx的响应头:

  • HX-Location - 允许您执行不会完全重新加载页面的客户端重定向
  • HX-Push-Url - 将新URL推入历史堆栈
  • HX-Redirect - 可用于客户端重定向到新位置
  • HX-Refresh - 如果设置为"true",客户端将完全刷新页面
  • HX-Replace-Url - 替换地址栏中的当前URL
  • HX-Reswap - 允许您指定响应将如何交换。参见hx-swap获取可能的值
  • HX-Retarget - 一个CSS选择器,将内容更新的目标更新为页面上的不同元素
  • HX-Reselect - 一个CSS选择器,允许您选择响应的哪部分用于交换。覆盖触发元素上现有的hx-select
  • HX-Trigger - 允许您触发客户端事件
  • HX-Trigger-After-Settle - 允许您在稳定步骤后触发客户端事件
  • HX-Trigger-After-Swap - 允许您在交换步骤后触发客户端事件

有关HX-Trigger头的更多信息,请参见HX-Trigger响应头

通过htmx提交表单的好处是不再需要Post/Redirect/Get模式。在服务器上成功处理POST请求后,不需要返回HTTP 302(重定向)。可以直接返回新的HTML片段。

此外,上述响应头不会与3xx重定向响应代码(如HTTP 302(重定向))一起提供给htmx处理。相反,浏览器将在内部拦截重定向并返回重定向URL的头和响应。尽可能使用替代响应代码(如200)以允许返回这些响应头。

请求操作顺序

htmx请求中的操作顺序为:

  • 元素被触发并开始请求
    • 收集请求的值
    • htmx-request类应用于适当的元素
    • 然后通过AJAX异步发出请求
      • 收到响应后,目标元素被标记为htmx-swapping
      • 应用可选的交换延迟(参见hx-swap属性)
      • 执行实际内容交换
        • 从目标中移除htmx-swapping
        • htmx-added类添加到每个新内容片段
        • htmx-settling类应用于目标
        • 进行稳定延迟(默认:20毫秒)
        • DOM稳定
        • 从目标中移除htmx-settling
        • 从每个新内容片段中移除htmx-added

您可以使用htmx-swappinghtmx-settling类在页面之间创建CSS过渡

验证

Htmx与HTML5验证API集成,如果可验证的输入无效,则不会发出表单请求。这对于AJAX请求和WebSocket发送都适用。

Htmx围绕验证触发事件,可用于挂钩自定义验证和错误处理:

  • htmx:validation:validate - 在调用元素的checkValidity()方法之前调用。可用于添加自定义验证逻辑
  • htmx:validation:failed - 当checkValidity()返回false时调用,表示无效输入
  • htmx:validation:halted - 当由于验证错误而未发出请求时调用。特定错误可以在event.detail.errors对象中找到

默认情况下,非表单元素在发出请求前不进行验证,但可以通过将hx-validate属性设置为"true"来启用验证。

验证示例

以下是使用hx-on属性捕获htmx:validation:validate事件并要求输入具有值foo的输入示例:

<form id="example-form" hx-post="/test">
    <input name="example"
           onkeyup="this.setCustomValidity('') // 在keyup时重置验证"
           hx-on:htmx:validation:validate="if(this.value != 'foo') {
                    this.setCustomValidity('请输入值foo') // 设置验证错误
                    htmx.find('#example-form').reportValidity()          // 报告问题
                }">
</form>

注意:所有客户端验证必须在服务器端重新完成,因为它们总是可以被绕过。

动画

Htmx允许您在许多情况下仅使用HTML和CSS实现CSS过渡效果。

有关可用选项的更多详情,请参阅动画指南

扩展

htmx提供了一个扩展机制,允许您自定义库的行为。扩展在JavaScript中定义,然后通过hx-ext属性启用。

核心扩展

htmx支持一些"核心"扩展,这些扩展由htmx开发团队支持:

您可以在扩展页面上查看所有可用扩展。

安装扩展

安装其他人创建的htmx扩展的最快方法是通过CDN加载它们。记得始终在扩展之前包含核心htmx库,并启用扩展。例如,如果想使用response-targets扩展,可以将以下内容添加到head标签中:

<head>
    <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js" integrity="sha384-Akqfrbj/HpNVo8k11SXBb6TlBWmXXlYQrCSqEWmyKJe+hDm3Z/B2WVG4smwBkRVm" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.2" integrity="sha384-UMuM7P2CPg9i2/dfvBlAeqjXITmEWe9k17Mp9X07Z4jXPN21Ychng569t+sUL8oa" crossorigin="anonymous"></script>
</head>
<body hx-ext="extension-name">
    ...

未压缩版本也可在https://cdn.jsdelivr.net/npm/htmx-ext-extension-name/dist/extension-name.js获取(将extension-name替换为扩展名)。

虽然CDN方法简单,但您可能需要考虑在生产中不使用CDN。安装htmx扩展的另一种简单方法是将其复制到项目中。从https://cdn.jsdelivr.net/npm/htmx-ext-extension-name下载扩展(将extension-name替换为扩展名),例如https://cdn.jsdelivr.net/npm/htmx-ext-response-targets。然后将其添加到项目中的适当目录,并在需要时使用<script>标签包含它。

对于npm风格的构建系统,可以通过npm安装htmx扩展(将extension-name替换为扩展名):

npm install htmx-ext-extension-name

安装后,需要使用适当的工具打包node_modules/htmx-ext-extension-name/dist/extension-name.js(或.min.js)。例如,您可以将扩展与来自node_modules/htmx.org/dist/htmx.js的htmx核心以及项目特定代码一起打包。

如果使用打包器管理JavaScript(例如Webpack、Rollup):

  • 通过npm安装htmx.orghtmx-ext-extension-name(将extension-name替换为扩展名)
  • 将两个包导入到index.js
import htmx.org;
import 'htmx-ext-extension-name'; // 将extension-name替换为扩展名 

注意:Idiomorph不遵循htmx扩展的命名约定。使用idiomorph而不是htmx-ext-idiomorph。例如,https://cdn.jsdelivr.net/npm/idiomorphnpm install idiomorph

注意:托管在此存储库之外的社区扩展可能有不同的安装说明。请查看相应的存储库以获取设置指导。

启用扩展

要启用扩展,请将hx-ext="extension-name"属性添加到<body>或其他HTML元素(将extension-name替换为扩展名)。扩展将应用于所有子元素。

以下示例展示了如何启用response-targets扩展,允许您基于HTTP响应代码指定不同的目标元素进行交换。

<body hx-ext="response-targets">
    ...
    <button hx-post="/register" hx-target="#response-div" hx-target-404="#not-found">
        注册!
    </button>
    <div id="response-div"></div>
    <div id="not-found"></div>
    ...
</body>

创建扩展

如果有兴趣向htmx添加自己的扩展,请参阅扩展文档

事件和日志

Htmx具有广泛的事件机制,同时也作为日志系统。

如果想注册给定的htmx事件,可以使用

document.body.addEventListener('htmx:load', function(evt) {
    myJavascriptLib.init(evt.detail.elt);
});

或者,如果愿意,可以使用以下htmx辅助函数:

htmx.on("htmx:load", function(evt) {
    myJavascriptLib.init(evt.detail.elt);
});

htmx:load事件在htmx每次将元素加载到DOM中时触发,实际上等同于普通的load事件。

htmx事件的一些常见用途包括:

使用事件初始化第三方库

使用htmx:load事件初始化内容非常常见,因此htmx提供了一个辅助函数:

htmx.onLoad(function(target) {
    myJavascriptLib.init(target);
});

这与第一个示例相同,但更简洁。

使用事件配置请求

可以处理htmx:configRequest事件以在发出AJAX请求之前修改它:

document.body.addEventListener('htmx:configRequest', function(evt) {
    evt.detail.parameters['auth_token'] = getAuthToken(); // 向请求中添加新参数
    evt.detail.headers['Authentication-Token'] = getAuthToken(); // 向请求中添加新头
});

这里我们在发送请求之前向请求中添加了一个参数和头。

使用事件修改交换行为

可以处理htmx:beforeSwap事件以修改htmx的交换行为:

document.body.addEventListener('htmx:beforeSwap', function(evt) {
    if(evt.detail.xhr.status === 404){
        // 当发生404时提醒用户(可能使用比alert()更好的机制)
        alert("错误:找不到资源");
    } else if(evt.detail.xhr.status === 422){
        // 允许422响应进行交换,因为我们使用此信号表示
        // 表单提交了错误数据,并希望重新渲染错误
        //
        // 将isError设置为false以避免在控制台中记录错误
        evt.detail.shouldSwap = true;
        evt.detail.isError = false;
    } else if(evt.detail.xhr.status === 418){
        // 如果返回响应代码418(我是茶壶),将响应的内容重定向到id为teapot的元素
        evt.detail.shouldSwap = true;
        evt.detail.target = htmx.find("#teapot");
    }
});

这里我们处理一些400级错误响应码,这些通常在htmx中不会触发交换。

事件命名

注意所有事件都会以两种不同的名称触发:

  • 驼峰式
  • 短横线式

例如,你可以监听htmx:afterSwaphtmx:after-swap。这有助于与其他库的互操作性。例如Alpine.js要求使用短横线式。

日志记录

如果在htmx.logger设置日志记录器,每个事件都会被记录。这对故障排查非常有用:

htmx.logger = function(elt, event, data) {
    if(console) {
        console.log(event, elt, data);
    }
}

调试

使用htmx(或其他声明式语言)进行声明式和事件驱动的编程是一种高效且美妙的活动,但与命令式方法相比的一个缺点是调试可能更棘手。

例如,如果不知道技巧,弄清楚为什么某事没有发生可能会很困难。

好吧,这里就是这些技巧:

第一个调试工具是htmx.logAll()方法。这将记录htmx触发的每个事件,让你准确看到库在做什么。

htmx.logAll();

当然,这不会告诉你htmx为什么没做某事。你可能也不知道DOM元素触发了什么事件可用作触发器。为解决这个问题,你可以使用浏览器控制台中的monitorEvents()方法:

monitorEvents(htmx.find("#theElement"));

这将把所有发生在id为theElement的元素上的事件输出到控制台,让你准确了解其情况。

注意这在控制台工作,不能嵌入到页面脚本标签中。

最后,如果实在不行,你可能需要加载未压缩版的htmx.js来调试。它大约有2500行JavaScript代码,量虽大但并非不可克服。你很可能需要在issueAjaxRequest()handleAjaxResponse()方法中设置断点来查看情况。

如果需要帮助,随时可以加入Discord

创建演示

有时为了演示错误或澄清用法,最好能使用像jsfiddle这样的JavaScript片段网站。为方便创建演示,htmx托管了一个演示脚本站点,将安装:

  • htmx
  • hyperscript
  • 请求模拟库

只需将以下脚本标签添加到你的演示/fiddle/任何地方:

<script src="https://demo.htmx.org"></script>

这个助手允许你通过添加带有url属性的template标签来添加模拟响应,指示对应URL。该URL的响应将是模板的innerHTML,便于构建模拟响应。你可以使用delay属性添加响应延迟,应为整数(毫秒)。

你可以使用${}语法在模板中嵌入简单表达式。

注意这只应用于演示,不保证长期有效,因为它总是获取最新版的htmx和hyperscript!

演示示例

以下是实际代码示例:

<!-- 加载演示环境 -->
<script src="https://demo.htmx.org"></script>

<!-- 提交到 /foo -->
<button hx-post="/foo" hx-target="#result">
    增加计数
</button>
<output id="result"></output>

<!-- 在template标签中用动态内容响应/foo -->
<script>
    globalInt = 0;
</script>
<template url="/foo" delay="500"> <!-- 注意url和delay属性 -->
    ${globalInt++}
</template>

脚本编写

虽然htmx鼓励采用超媒体方法构建Web应用,但它提供了多种客户端脚本编写选项。脚本编写包含在Web架构的REST描述中,参见:按需代码。我们建议尽可能在Web应用中采用超媒体友好的脚本编写方法:

htmx与脚本解决方案的主要集成点是htmx发送和响应的事件。参见第三方JavaScript部分的SortableJS示例,了解通过事件将JavaScript库与htmx集成的良好模板。

与htmx配合良好的脚本解决方案包括:

  • VanillaJS - 简单地使用JavaScript内置能力挂钩事件处理程序来响应htmx发出的事件非常有效。这是一种极其轻量且日益流行的方法。
  • AlpineJS - Alpine.js提供了一套丰富的工具来创建复杂的前端脚本,包括响应式编程支持,同时保持极其轻量。Alpine鼓励"内联脚本"方法,我们认为这与htmx搭配良好。
  • jQuery - 尽管在某些圈子里有年代感和声誉,但jQuery与htmx配合良好,特别是在已有大量jQuery的旧代码库中。
  • hyperscript - Hyperscript是由创建htmx的同一团队创建的实验性前端脚本语言。它设计为能很好地嵌入HTML中,既能响应事件也能创建事件,与htmx配合极佳。

我们在我们的书中有一个完整章节"客户端脚本编写",探讨如何将脚本集成到基于htmx的应用中。

hx-on*属性

HTML允许通过onevent属性嵌入内联脚本,例如onClick

<button onclick="alert('你点击了我!')">
    点我!
</button>

此功能允许脚本逻辑与所应用的HTML元素共置,提供良好的行为局部性(LoB)。遗憾的是,HTML只允许为固定数量的特定DOM事件(如onclick)使用on*属性,并未提供通用机制来响应元素上的任意事件。

为解决此缺陷,htmx提供了hx-on*属性。这些属性允许你响应任何事件,同时保持标准on*属性的LoB。

如果我们想使用hx-on属性响应click事件,可以这样写:

<button hx-on:click="alert('你点击了我!')">
    点我!
</button>

即字符串hx-on后跟冒号(或短横线),然后是事件名称。

对于click事件,我们当然建议坚持使用标准onclick属性。但考虑一个使用htmx的按钮,希望通过htmx:config-request事件向请求添加参数。使用标准on*属性无法实现,但可通过hx-on:htmx:config-request属性完成:

<button hx-post="/example"
        hx-on:htmx:config-request="event.detail.parameters.example = '你好脚本!'">
    提交我!
</button>

这里在发出POST请求前将example参数添加到了请求中,值为'你好脚本!'。

另一个用例是使用afterRequest事件在成功请求后重置用户输入,避免使用out of band交换。

hx-on*属性是用于通用嵌入式脚本的非常简单的机制。它不是更成熟的前端脚本解决方案(如AlpineJS或hyperscript)的替代品。但它可以增强基于VanillaJS的脚本方法在你的htmx应用中的能力。

注意HTML属性不区分大小写。这意味着不幸的是,无法响应依赖大写/驼峰式命名的事件。如果需要支持驼峰式事件,我们建议使用功能更全面的脚本解决方案,如AlpineJS或hyperscript。htmx为所有事件同时分发驼峰式(camelCase)和短横线式(kebab-case)正是出于这个原因。

第三方JavaScript

Htmx与第三方库集成相当好。如果库在DOM上触发事件,你可以使用这些事件从htmx触发请求。

一个很好的例子是SortableJS演示

<form class="sortable" hx-post="/items" hx-trigger="end">
    <div class="htmx-indicator">更新中...</div>
    <div><input type='hidden' name='item' value='1'/>项目1</div>
    <div><input type='hidden' name='item' value='2'/>项目2</div>
    <div><input type='hidden' name='item' value='2'/>项目3</div>
</form>

与大多数JavaScript库一样,使用Sortable时需要在某个时刻初始化内容。

在jquery中,你可能会这样做:

$(document).ready(function() {
    var sortables = document.body.querySelectorAll(".sortable");
    for (var i = 0; i < sortables.length; i++) {
        var sortable = sortables[i];
        new Sortable(sortable, {
            animation: 150,
            ghostClass: 'blue-background-class'
        });
    }
});

在htmx中,你会改用htmx.onLoad函数,并且只从新加载的内容中选择,而不是整个文档:

htmx.onLoad(function(content) {
    var sortables = content.querySelectorAll(".sortable");
    for (var i = 0; i < sortables.length; i++) {
        var sortable = sortables[i];
        new Sortable(sortable, {
            animation: 150,
            ghostClass: 'blue-background-class'
        });
    }
})

这将确保当htmx向DOM添加新内容时,可排序元素被正确初始化。

如果JavaScript向DOM添加了带有htmx属性的内容,你需要确保使用htmx.process()函数初始化此内容。

例如,如果你使用fetch API获取一些数据并将其放入div中,且该HTML中包含htmx属性,你需要添加对htmx.process()的调用,如下所示:

let myDiv = document.getElementById('my-div')
fetch('http://example.com/movies.json')
    .then(response => response.text())
    .then(data => { myDiv.innerHTML = data; htmx.process(myDiv); } );

某些第三方库从HTML模板元素创建内容。例如,Alpine JS在模板上使用x-if属性有条件地添加内容。此类模板最初不是DOM的一部分,如果它们包含htmx属性,则需要在加载后调用htmx.process()。以下示例使用Alpine的$watch函数查找会触发条件内容的值更改:

<div x-data="{show_new: false}"
    x-init="$watch('show_new', value => {
        if (show_new) {
            htmx.process(document.querySelector('#new_content'))
        }
    })">
    <button @click = "show_new = !show_new">切换新内容</button>
    <template x-if="show_new">
        <div id="new_content">
            <a hx-get="/server/newstuff" href="#">新可点击项</a>
        </div>
    </template>
</div>

Web组件

请参阅Web组件示例页面,了解如何将htmx与Web组件集成的示例。

缓存

htmx开箱即用地支持标准HTTP缓存机制。

如果你的服务器为给定URL的响应添加了Last-ModifiedHTTP响应头,浏览器会自动将If-Modified-Since请求HTTP头添加到同一URL的下一个请求中。请注意,如果你的服务器可以根据某些其他头为同一URL呈现不同内容,则需要使用Vary响应HTTP头。例如,如果你的服务器在缺少HX-Request头或其值为false时呈现完整HTML,而在HX-Request: true时呈现该HTML的片段,则需要添加Vary: HX-Request。这将导致缓存基于响应URL和HX-Request请求头的组合进行键控——而不仅仅是基于响应URL。始终禁用htmx.config.historyRestoreAsHxRequest,以便这些历史记录完整HTML请求不会与部分片段响应一起缓存。

如果你无法(或不愿意)使用Vary头,可以替代地将配置参数getCacheBusterParam设置为true。如果设置了此配置变量,htmx将在其发出的GET请求中包含一个缓存破坏参数,格式为org.htmx.cache-buster=targetElementId,以防止浏览器将基于htmx和非基于htmx的响应缓存在同一缓存槽中。

htmx也与ETag如预期一样工作。请注意,如果你的服务器可以为同一URL呈现不同内容(例如,取决于HX-Request头的值),服务器需要为每个内容生成不同的ETag

安全

htmx允许你直接在DOM中定义逻辑。这有许多优点,最大的是行为局部性(LoB),这使你的系统更易于理解和维护。

然而,这种方法的一个担忧是安全性:由于htmx增加了HTML的表现力,如果恶意用户能够将HTML注入你的应用程序,他们可以利用htmx的这种表现力达到恶意目的。

规则1:转义所有用户内容

基于HTML的Web开发的第一个规则始终是:不要信任来自用户的输入。你应该转义所有注入到你站点的第三方、不受信任的内容。这是为了防止包括XSS攻击在内的问题。

关于XSS以及如何预防它,在优秀的OWASP网站上有大量文档,包括跨站脚本预防备忘单

好消息是这是一个非常古老且被充分理解的主题,绝大多数服务器端模板语言都支持自动转义内容来防止此类问题。

话虽如此,有时人们会选择更危险地注入HTML,通常通过其模板语言中的某种raw()机制。这可能出于正当理由,但如果注入的内容来自第三方,则必须进行清理,包括删除以hx-data-hx开头的属性,以及内联<script>标签等。

如果你正在注入原始HTML并进行自己的转义,最佳实践是允许列表允许的属性和标签,而不是禁止列表不允许的。

htmx安全工具

当然,错误会发生,开发人员也不完美,因此最好对你的Web应用程序采用分层安全方法,htmx也提供了工具来帮助保护你的应用程序。

让我们来看看它们。

hx-disable

htmx提供的第一个工具是hx-disable属性。此属性将阻止处理给定元素及其内所有元素的htmx属性。例如,如果你在模板中包含原始HTML内容(再次强调,不建议这样做!),你可以在内容周围放置一个带有hx-disable属性的div:

<div hx-disable>
    <%= raw(user_content) %>
</div>

而htmx将不会处理在该内容中找到的任何htmx相关属性或功能。此属性无法通过注入更多内容来禁用:如果在元素的父层次结构中的任何位置找到hx-disable属性,htmx将不会处理它。

hx-history

另一个安全考虑是htmx历史缓存。你可能有一些包含敏感数据的页面,不希望存储在用户的localStorage缓存中。你可以通过在页面上的任何位置包含hx-history属性并将其值设置为false来从历史缓存中省略该页面。

配置选项

htmx还提供与安全相关的配置选项:

  • htmx.config.selfRequestsOnly - 如果设置为true,则仅允许对与当前文档相同域的请求
  • htmx.config.allowScriptTags - htmx将处理在其加载的新内容中找到的<script>标签。如果你希望禁用此行为,可以将此配置变量设置为false
  • htmx.config.historyCacheSize - 可以设置为0以避免在localStorage缓存中存储任何HTML
  • htmx.config.allowEval - 可以设置为false以禁用htmx所有依赖eval的功能:
    • 事件过滤器
    • hx-on:属性
    • js:前缀的hx-vals
    • js:前缀的hx-headers

请注意,禁用eval()所移除的所有功能都可以使用你自己的自定义JavaScript和htmx事件模型重新实现。

事件

如果你想允许除当前主机外某些域的请求,但不是完全开放,可以使用htmx:validateUrl事件。此事件将在detail.url槽中提供请求URL,以及一个sameHost属性。

你可以检查这些值,如果请求无效,在事件上调用preventDefault()以阻止发出请求。

document.body.addEventListener('htmx:validateUrl', function (evt) {
  // 仅允许对当前服务器以及myserver.com的请求
  if (!evt.detail.sameHost && evt.detail.url.hostname !== "myserver.com") {
    evt.preventDefault();
  }
});

CSP选项

浏览器也提供工具来进一步保护你的Web应用程序。最强大的可用工具是内容安全策略(CSP)。使用CSP,你可以告诉浏览器,例如,不要向非原始主机发出请求,不要评估内联脚本标签等。

以下是meta标签中的CSP示例:

    <meta http-equiv="Content-Security-Policy" content="default-src 'self';">

这告诉浏览器"仅允许连接到原始(源)域"。这与htmx.config.selfRequestsOnly是冗余的,但在处理应用程序安全时,分层安全方法是必要的,事实上也是理想的。

关于CSP的完整讨论超出了本文档的范围,但MDN文章为探索此主题提供了良好的起点。

CSRF预防

CSRF令牌的分配和检查通常是后端的责任,但htmx可以使用hx-headers属性支持在每个请求中自动返回CSRF令牌。该属性需要添加到发出请求的元素或其祖先元素之一。这使得htmlbody元素成为将CSRF令牌添加到HTTP请求头的有效全局载体,如下所示。

注意:hx-boost不会更新<html><body>标签;如果在hx-boost中使用此功能,请确保在被替换的元素上包含CSRF令牌。许多Web框架支持自动将CSRF令牌作为隐藏输入插入HTML表单中。只要可能,都鼓励这样做。

<html lang="en" hx-headers='{"X-CSRF-TOKEN": "在此处插入CSRF令牌"}'>
    :
</html>
    <body hx-headers='{"X-CSRF-TOKEN": "在此处插入CSRF令牌"}'>
        :
    </body>

上述元素通常在HTML文档中是唯一的,应该很容易在模板中找到。

配置htmx

Htmx有一些配置选项,可以通过编程方式或声明方式访问。它们列在下面:

配置变量信息
htmx.config.historyEnabled默认为true,仅对测试有用
htmx.config.historyCacheSize默认为10
htmx.config.refreshOnHistoryMiss默认为false,如果设置为true,htmx将在历史记录未命中时执行完整页面刷新,而不是使用AJAX请求
htmx.config.defaultSwapStyle默认为innerHTML
htmx.config.defaultSwapDelay默认为0
htmx.config.defaultSettleDelay默认为20
htmx.config.includeIndicatorStyles默认为true(决定是否加载指示器样式)
htmx.config.indicatorClass默认为htmx-indicator
htmx.config.requestClass默认为htmx-request
htmx.config.addedClass默认为htmx-added
htmx.config.settlingClass默认为htmx-settling
htmx.config.swappingClass默认为htmx-swapping
htmx.config.allowEval默认为true,可用于禁用htmx对某些功能(例如触发器过滤器)使用eval
htmx.config.allowScriptTags默认为true,决定htmx是否处理在新内容中找到的脚本标签
htmx.config.inlineScriptNonce默认为'',表示不会向内联脚本添加nonce
htmx.config.attributesToSettle默认为["class", "style", "width", "height"],在稳定阶段要稳定的属性
htmx.config.inlineStyleNonce默认为'',表示不会向内联样式添加nonce
htmx.config.useTemplateFragments默认为false,使用HTML模板标签解析服务器内容(不兼容IE11!)
htmx.config.wsReconnectDelay默认为full-jitter
htmx.config.wsBinaryType默认为blob,通过WebSocket连接接收的二进制数据类型
htmx.config.disableSelector默认为[hx-disable], [data-hx-disable],htmx不会处理带有此属性或其父元素带有此属性的元素
htmx.config.withCredentials默认为false,允许使用凭据(如cookie、授权头或TLS客户端证书)进行跨站点Access-Control请求
htmx.config.timeout默认为0,请求在自动终止前可以花费的毫秒数
htmx.config.scrollBehavior默认为'instant',使用显示修饰符与hx-swap时的滚动行为。允许的值为instant(滚动应立即在单次跳转中发生)、smooth(滚动应平滑动画)和auto(滚动行为由scroll-behavior的计算值确定)。
htmx.config.defaultFocusScroll是否应将聚焦元素滚动到视图中,默认为false,可以使用焦点滚动交换修饰符覆盖。
htmx.config.getCacheBusterParam默认为false,如果设置为true,htmx将以org.htmx.cache-buster=targetElementId格式将目标元素附加到GET请求中
htmx.config.globalViewTransitions如果设置为true,htmx将在交换新内容时使用视图过渡API
htmx.config.methodsThatUseUrlParams默认为["get", "delete"],htmx将通过将参数编码在URL中而不是请求正文中来格式化这些方法的请求
htmx.config.selfRequestsOnly默认为true,是否仅允许向与当前文档相同域的AJAX请求
htmx.config.ignoreTitle默认为false,如果设置为true,当在新内容中找到title标签时,htmx将不会更新文档标题
htmx.config.disableInheritance禁用htmx中的属性继承,然后可由hx-inherit属性覆盖
htmx.config.scrollIntoViewOnBoost默认为true,是否将增强元素的目标滚动到视口中。如果在增强元素上省略hx-target,目标默认为body,导致页面滚动到顶部。
htmx.config.triggerSpecsCache默认为null,用于存储评估的触发器规范的缓存,以提高解析性能,但代价是更多内存使用。你可以定义一个简单对象来使用永不清除的缓存,或使用代理对象实现你自己的系统
htmx.config.responseHandling可以在此处配置响应状态码的默认响应处理行为为交换或错误
htmx.config.allowNestedOobSwaps默认为true,是否处理嵌套在主响应元素中的元素的OOB交换。参见嵌套OOB交换
htmx.config.historyRestoreAsHxRequest默认为true,是否将历史缓存未命中的完整页面重新加载请求视为"HX-Request"通过返回此响应头。当使用HX-Request头选择性地返回部分响应时,应始终禁用此功能

你可以直接在JavaScript中设置它们,也可以使用meta标签:

<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>

结论

就是这样!

享受htmx的乐趣吧!无需编写大量代码,你就能完成相当多的事情