文档

#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 仍然支持并支持 IE11。

#从 1.x 到 2.x 的迁移指南

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

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

#安装

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

#通过 CDN (例如 unpkg.com)

最快的使用 htmx 的方式是通过 CDN 加载。你只需将以下代码添加到你的头标签中即可开始使用:

<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>

还有一个未压缩的版本可以用于调试:

<script src="https://unpkg.com/htmx.org@2.0.2/dist/htmx.js" integrity="sha384-yZq+5izaUBKcRgFbxgkRYwpHhHHCpp5nseXp0MEQ1A4MTWVMnqkmcuFez8x5qfxr" crossorigin="anonymous"></script>

虽然 CDN 方式极其简单,但你可能需要考虑不在生产环境中使用 CDN

#下载副本

下一个最简单的安装 htmx 的方式是直接将其复制到你的项目中。

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

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

#npm

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

npm install htmx.org@2.0.2

安装后,你需要使用适当的工具来使用 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">
    发送 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 选择器不会重新计算。

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

<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 键点击元素时触发的示例:

<div hx-get="/clicked" hx-trigger="click[ctrlKey]">
    Control Click Me
</div>

ctrlKey 这样的属性将首先根据触发事件进行解析,然后才是全局范围。this 符号将被设置为当前元素。

#特殊事件

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

  • load - 元素首次加载时触发一次
  • revealed - 元素首次滚动进入视口时触发一次
  • intersect - 元素首次与视口相交时触发一次。此事件支持两个附加选项:
    • root:<selector> - 用于指定交叉观察器的根元素的 CSS 选择器
    • threshold:<float> - 一个介于 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">
    Click Me!
    <img class="htmx-indicator" src="/spinner.gif">
</button>

这里我们有一个按钮。当点击它时,htmx-request 类将被添加到它上面,从而显示 spinner 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">
        Click Me!
    </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="Search..."
>
<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">Like</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">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">Submit</button>
</form>

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

htmx 还支持一种编程方式来取消请求:你可以向某个元素发送 htmx:abort 事件来取消任何正在进行的请求:

<button id="request-button" hx-post="/example">
    Issue Request
</button>
<button onclick="htmx.trigger('#request-button', 'htmx:abort')">
    Cancel Request
</button>

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

#CSS 过渡

htmx 使得在不使用 JavaScript 的情况下使用 CSS 过渡 变得容易。考虑以下 HTML 内容:

<div id="div1">Original Content</div>

假设此内容通过 htmx 通过 ajax 请求替换为以下新内容:

<div id="div1" class="red">New Content</div>

注意两点:

  • div 在原始内容和新内容中具有相同的 id
  • red 类已添加到新内容中

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

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

当 htmx 替换新内容时,它会以适用 CSS 过渡的方式进行替换,从而使你获得一个平滑的状态过渡。

总之,要在元素中使用 CSS 过渡,只需确保其 id 在请求之间保持稳定即可!

你可以查看 动画示例 以获取更多详细信息和现场演示。

#详细信息

要理解 CSS 过渡在 htmx 中的实际工作原理,你必须理解 htmx 使用的底层替换和安置模型。

当从服务器接收到新内容时,在替换内容之前,页面的现有内容会被检查是否有与 id 属性匹配的元素。如果在新内容中找到匹配项,旧内容的属性会在替换发生前复制到新元素上。然后新内容会被替换进来,但具有 的属性值。最后,在“安置”延迟(默认情况下为 20ms)之后,新属性值会被替换进来。听起来有点疯狂,但正是这种机制使得开发者在无需任何 JavaScript 的情况下实现了 CSS 过渡。

#带外替换

如果你想通过使用 id 属性直接将响应内容替换到 DOM 中,你可以在 响应 HTML 中使用 [hx-swap-oob](@/attributes/hx-swap-oob

.md) 属性:

<div id="message" hx-swap-oob="true">Swap me directly!</div>
Additional Content

在此响应中,div#message 将被直接替换到匹配的 DOM 元素中,而附加内容将以正常方式替换到目标中。

你可以使用这种技术来“搭载”其他请求的更新。

#棘手的表格

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

为避免此问题,可以使用 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 请求,最近的包含表单内的所有输入项的值也将被包含在请求中。

如果你希望包含其他元素的值,可以使用 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="Are you sure you wish to delete your account?">
    Delete My Account
</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: "Are you sure?",
      text: "Are you sure you are sure?",
      icon: "warning",
      buttons: true,
      dangerMode: true,
    }).then((confirmed) => {
      if (confirmed) {
        evt.detail.issueRequest();
      }
    });
  }
});

#属性继承

htmx 中的大多数属性是可继承的:它们适用于它们所在的元素以及任何子元素。这使得你可以将属性“提升”到 DOM 上级元素,以避免代码重复。请看以下 htmx 示例:

<button hx-delete="/account" hx-confirm="Are you sure?">
    Delete My Account
</button>
<button hx-put="/account" hx-confirm="Are you sure?">
    Update My Account
</button>

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

<div hx-confirm="Are you sure?">
    <button hx-delete="/account">
        Delete My Account
    </button>
    <button hx-put="/account">
        Update My Account
    </button>
</div>

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

有时你可能希望取消这种继承。例如,如果我们在这组按钮中添加一个取消按钮,但不希望它被确认。我们可以在其上添加一个 unset 指令,如下所示:

<div hx-confirm="Are you sure?">
    <button hx-delete="/account">
        Delete My Account
    </button>
    <button hx-put="/account">
        Update My Account
    </button>
    <button hx-confirm="unset" hx-get="/">
        Cancel
    </button>
</div>

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

可以使用 hx-disinherit 属性,在每个元素和每个属性的基础上禁用继承。

如果你希望完全禁用属性继承,可以将 htmx.config.disableInheritance 配置变量设置为 true。这将禁用默认的继承,并允许你通过 hx-inherit 属性明确指定继承。

#增强

htmx 支持通过 hx-boost 属性对常规 HTML 锚点和表单进行“增强”。此属性将把所有锚点标签和表单转换为 AJAX 请求,并默认将响应目标设置为页面的主体部分。

这是一个示例:

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

此 div 中的锚点标签将发出一个 AJAX GET 请求到 /blog,并将响应内容替换到 body 标签内。

#渐进增强

hx-boost 的一个特点是,如果没有启用 JavaScript,它会优雅地降级:链接和表单继续工作,只是它们不再使用 AJAX 请求。这就是所谓的 渐进增强,它允许更广泛的受众使用你网站的功能。

其他 htmx 模式也可以调整以实现渐进增强,但这需要更多的思考。

主动搜索 示例为例。按目前的写法,它不会优雅地降级:那些没有启用 JavaScript 的用户将无法使用此功能。这样做是为了简化示例,使其尽可能简洁。

然而,你可以将 htmx 增强的输入元素包装在一个表单元素中:

<form action="/search" method="POST">
    <input class="form-control" type="search"
        name="search" placeholder="Begin typing to search users..."
        hx-post="/search"
        hx-trigger="keyup changed delay:500ms, search"
        hx-target="#search-results"
        hx-indicator=".htmx-indicator">
</form>

这样一来,启用 JavaScript 的客户端仍然会获得良好的主动搜索用户体验,而未启用 JavaScript 的客户端可以通过按回车键进行搜索。更好的是,你还可以添加一个“搜索”按钮。然后你需要在表单上更新一个 hx-post,使其与 action 属性一致,或者在表单上使用 hx-boost

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

其他模式也可以类似调整,以满足你的应用程序的渐进增强需求。

如你所见,这需要更多的思考和工作。这也完全排除了某些功能。这些权衡必须由你,即开发人员,根据你的项目目标和受众做出。

无障碍性 是一个与渐进增强密切相关的概念。使用像 hx-boost 这样的渐进增强技术,将使你的 htmx 应用程序对更广泛的用户更具无障碍性。

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

因此,常规的 HTML 无障碍性建议同样适用于 htmx 应用程序。例如:

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

#WebSockets 和 SSE

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

#历史记录支持

Htmx 提供了一种简单的机制与 浏览器历史记录 API 进行交互:

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

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

当用户点击这个链接时,htmx 将会在发出对 /blog 的请求之前,快照当前的 DOM 并存储它。然后,它会执行内容替换并将新的位置推送到历史堆栈中。

当用户点击后退按钮时,htmx 会从存储中检索旧的内容,并将其替换回目标区域,模拟“返回”到之前的状态。如果在缓存中找不到该位置,htmx 将向给定的 URL 发出一个带有 HX-History-Restore-Request 标头的 Ajax 请求,并期望返回整个页面所需的 HTML。或者,如果 htmx.config.refreshOnHistoryMiss 配置变量设置为 true,它将进行硬刷新。

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

#指定历史快照元素

默认情况下,htmx 将使用 body 来进行历史快照的获取和恢复。这通常是正确的做法,但如果你想使用更小范围的元素进行快照,可以使用 hx-history-elt 属性指定一个不同的元素。

注意:这个元素需要在所有页面上都存在,否则从历史记录恢复时将无法正常工作。

#撤销第三方库的 DOM 变更

如果你使用了第三方库,并希望使用 htmx 的历史记录功能,你需要在快照之前清理 DOM。让我们考虑 Tom Select 库,它使 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() 方法
})

这将 DOM 恢复到原始的 HTML,从而允许进行干净的快照。

#禁用历史快照

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

#请求与响应

Htmx 期望其发出的 AJAX 请求的响应为 HTML,通常是 HTML 片段(虽然配合 hx-select 标签使用时,完整的 HTML 文档也很有用)。Htmx 会将返回的 HTML 替换到指定的目标位置,并使用指定的替换策略。

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

对于这种情况,默认情况下,你可以返回 204 - No Content 响应码,htmx 将忽略响应内容。

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

在发生连接错误时,将触发 htmx:sendError 事件。

#配置响应处理

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

    responseHandling: [
        {code:"204", swap: false},   // 204 - No Content 默认不执行任何操作,但不视为错误
        {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 应忽略响应中的标题标签,则为 true
  • select - 用于从响应中选择内容的 CSS 选择器
  • target - 指定响应替换的目标的 CSS 选择器
  • swapOverride - 替换响应内容的替代机制

#配置响应处理示例

作为使用此配置的示例,考虑一个服务器端框架在验证错误发生时返回 422 - Unprocessable Entity 响应的情况。默认情况下,htmx 将忽略响应,因为它与正则表达式 [45].. 匹配。

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

<!--
  * 204 No Content 默认不执行任何操作,但不视为错误
  * 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}]}' /> <!--所有响应均进行替换-->

最后,值得考虑使用 响应目标 扩展,该扩展允许你通过属性声明式地配置响应码的行为。

#跨域资源共享 (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”
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 片段。

#请求操作顺序

htmx 请求的操作顺序如下:

  • 元素被触发并开始请求
    • 收集请求的值
    • htmx-request 类被应用到相关元素上
    • 请求通过 AJAX 异步发出
      • 在接收到响应时,目标元素被标记为 htmx-swapping
      • 应用可选的交换延迟(请参阅 hx-swap 属性)
      • 执行实际内容交换
        • 从目标中移除 htmx-swapping
        • htmx-added 类添加到每个新内容中
        • htmx-settling 类应用到目标中
        • 执行定居延迟(默认:20ms)
        • 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('') // 在按键时重置验证"
           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 属性使用:

<div hx-ext="debug">
    <button hx-post="/example">此按钮使用了 debug 扩展</button>
    <button hx-post="/example" hx-ext="ignore:debug">此按钮未使用</button>
</div>

如果您有兴趣向 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 将元素加载到 DOM 中时,都会触发 htmx:load 事件,这实际上相当于正常的 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 中执行交换。

#事件命名

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

  • 驼峰式命名法 (Camel Case)
  • 短横线命名法 (Kebab Case)

因此,例如,您可以监听 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
  • 请求模拟库

只需将以下脚本标签添加到您的演示/小提琴/其他内容中:

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

此助手允许您通过添加 template 标签并使用 url 属性指示哪个 URL 来添加模拟响应。该 URL 的响应将是 template 的 innerHTML,从而使构建模拟响应变得容易。您可以使用 delay 属性为响应添加延迟,该属性应为一个整数,表示延迟的毫秒数。

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

请注意,此功能仅用于演示,并且无法保证长期有效,因为它将始终获取 htmx 和 hyperscript 的最新版本!

#演示示例

以下是代码的实际示例:

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

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

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

#脚本编写

虽然 htmx 鼓励使用超媒体方法构建 Web 应用程序,但它为客户端脚本提供了许多选项。脚本编写包括在 Web 架构的 REST-ful 描述中,参见:按需编码。尽可能多地推荐在 Web 应用程序中采用 超媒体友好 的脚本编写方法:

htmx 与脚本解决方案之间的主要集成点是 htmx 发送并响应的事件。请参阅 第三方 JavaScript 部分中的 SortableJS 示例,以获取通过事件与 htmx 集成 JavaScript 库的良好模板。

与 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 仅允许

on* 属性用于固定数量的 特定 DOM 事件(例如 onclick),并且不提供响应元素上的任意事件的通用机制。

为了解决这个问题,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 = 'Hello Scripting!'">
    提交我!
</button>

在这里,在发出 POST 请求之前添加了 example 参数,值为 ‘Hello Scripting!’。

hx-on* 属性是一种用于通用嵌入式脚本的非常简单的机制。它不是更完善的前端脚本解决方案(如 AlpineJS 或 hyperscript)的替代品。然而,它可以增强基于 VanillaJS 的脚本编写方法在 htmx 驱动的应用程序中的应用。

请注意,HTML 属性是不区分大小写的。这意味着,不幸的是,依赖于大写/驼峰式命名法的事件无法响应。如果您需要支持驼峰式命名事件,我们建议使用更完善的脚本解决方案,如 AlpineJS 或 hyperscript。htmx 为此原因同时触发驼峰式命名法和短横线命名法的事件。

#第三方 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>

与 Sortable 类似,大多数 JavaScript 库需要在某个时刻初始化内容。

在 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-Modified HTTP 响应标头,浏览器将自动向下次对相同 URL 的请求添加 If-Modified-Since 请求 HTTP 标头。请注意,如果您的服务器可以根据某些其他标头为同一 URL 渲染不同的内容,您需要使用 Vary 响应 HTTP 标头。例如,如果您的服务器在 HX-Request 标头丢失或为 false 时渲染完整 HTML,而在 HX-Request: true 时渲染该 HTML 的片段,您需要添加 Vary: HX-Request。这会导致缓存基于响应 URL 和 HX-Request 请求标头的组合进行密钥分配,而不是仅基于响应 URL。

如果您无法(或不愿意)使用 Vary 标头,您可以选择将配置参数 getCacheBusterParam 设置为 true。如果设置了此配置变量,htmx 将在它发出的 GET 请求中包含一个缓存破坏参数,这将防止浏览器在相同的缓存槽中缓存基于 htmx 的响应和非 htmx 的响应。

htmx 还与 ETag 正常工作。请注意,如果您的服务器可以为同一 URL 渲染不同的内容(例如,取决于 HX-Request 标头的值),则服务器需要为每个内容生成不同的 ETag

#安全性

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

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

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

HTML 的 Web 开发的第一条规则始终是:不要信任用户的输入。您应该转义注入到您网站中的所有第三方、不受信任的内容。这是为了防止,包括在内的,跨站脚本攻击 (XSS)

在优秀的 OWASP 网站 上有关于 XSS 及其防止的广泛文档,包括 跨站脚本防护备忘单

好消息是,这是一个非常古老且广为人知的话题,大多数服务器端模板语言都支持 自动转义 内容以防止此类问题。

即便如此,有时人们选择以更危险的方式注入 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 应用程序的工具。最强大的工具是 内容安全策略 (Content Security Policy, CSP)。使用 CSP,您可以指示浏览器,例如,不要向非来源主机发出请求,不要评估内联脚本标签等。

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

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

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

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

#配置 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接收的二进制数据类型
htmx.config.disableSelector默认为 [hx-disable], [data-hx-disable],htmx 将不会处理带有此属性的元素或其父元素
htmx.config.withCredentials默认为 false,允许跨站点使用凭据(如 cookies、授权标头或 TLS 客户端证书)进行 Access-Control 请求
htmx.config.timeout默认为 0,请求在自动终止之前可以进行的毫秒数
htmx.config.scrollBehavior默认为 ‘smooth’,页面过渡中加速链接的行为。允许的值为 autosmooth。Smooth 将平滑滚动到页面顶部,而 auto 将表现为普通链接。
htmx.config.defaultFocusScroll是否应滚动到视图中的聚焦元素,默认为 false,可以使用 focus-scroll 交换修饰符进行覆盖。
htmx.config.getCacheBusterParam默认为 false,如果设置为 true,htmx 将在 GET 请求中附加目标元素,格式为 org.htmx.cache-buster=targetElementId
htmx.config.globalViewTransitions如果设置为 true,htmx 将在交换新内容时使用 View Transition API。
htmx.config.methodsThatUseUrlParams默认为 ["get"],htmx 将使用这些方法通过在 URL 中编码其参数而不是请求主体来格式化请求
htmx.config.selfRequestsOnly默认为 true,是否仅允许 AJAX 请求到与当前文档相同的域
htmx.config.ignoreTitle默认为 false,如果设置为 true,htmx 将在新内容中找到 title 标签时不会更新文档标题
htmx.config.disableInheritance禁用 htmx 中的属性继承,然后可以通过 hx-inherit 属性进行覆盖
htmx.config.scrollIntoViewOnBoost默认为 true,是否将加速元素的目标滚动到视图中。如果加速元素上省略了 hx-target,目标默认为 body,导致页面滚动到顶部。
htmx.config.triggerSpecsCache默认为 null,用于存储已评估触发器规格的缓存,提高解析性能,代价是更多的内存使用。您可以定义一个简单的对象来使用永不清除的缓存,或者使用 代理对象 实现自己的系统。
htmx.config.allowNestedOobSwaps默认为 true,是否处理嵌套在主响应元素内的 OOB 交换。请参阅 嵌套 OOB 交换

您可以直接在 JavaScript 中设置这些选项,或者使用 meta 标签:

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

#结论

这就是全部内容!

享受使用 htmx 的乐趣吧!您可以在不编写大量代码的情况下

完成很多事情