htmx 简介
htmx 是一个 javascript 库,允许你直接从 HTML 访问现代浏览器功能,而不是使用 javascript 代码。
为了理解 htmx,首先我们来看一下 <a> 标签:
<a href="/blog">Blog</a>
这个 <a> 标签告诉浏览器:
当用户点击此链接时,向‘/blog’发出 HTTP GET 请求,并将响应内容加载到浏览器窗口中。
考虑到这一点,请参考以下 HTML:
<button hx-post="/clicked"
hx-trigger="click"
hx-target="#parent-div"
hx-swap="outerHTML">
Click Me!
</button>
这告诉 htmx:
当用户点击此按钮时,向 '/clicked' 发出 HTTP POST 请求,并使用响应中的内容替换 DOM 中 id 为 parent-div 的元素
htmx 扩展并概括了 HTML 作为超文本的核心思想,直接在语言内部开辟了更多的可能性:
- 现在任何元素(不仅仅是锚点和表单)都可以发出 HTTP 请求
- 现在,任何事件(不仅仅是点击或表单提交)都可以触发请求
- 现在可以使用任何 HTTP 动词,而不仅仅是 GET 和 POST
- 现在,任何元素(而不仅仅是整个窗口)都可以成为请求更新的目标
请注意,当你使用 htmx 时,在服务器端你通常使用 HTML 而不是 JSON 进行响应。这让你牢牢地保持在原始的 Web 编程模型中,使用超文本作为应用程序状态的引擎, 甚至不需要真正理解这个概念。
值得一提的是,如果你愿意,你可以在使用 htmx 时使用 data- 前缀:
<a data-hx-post="/click">Click Me!</a>
最后, htmx 1.0版本依然受支持,并且支持IE11。
1.x 到 2.x 迁移指南
如果你要从htmx 1.x 迁移到 htmx 2.x ,请参阅 htmx 1.x 迁移指南。
如果你要从 intercooler.js 迁移到 htmx,请参阅 intercooler 迁移指南。
安装
htmx是一个无依赖、面向浏览器的javascript库。这意味着使用它就像 <script> 在 HTML head 中添加标签一样简单。使用它不需要构建系统。
通过CDN加载(例如 unpkg.com )
使用htmx的最快方法是通过CDN加载。你只需将其添加到 head 标签即可使用。
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
还有未压缩的版本可用:
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.js" integrity="sha384-BBDmZzVt6vjz5YbQqZPtFZW82o8QotoM7RUp5xOxV3nSJ8u2pSdtzFAbGKzTlKtg" crossorigin="anonymous"></script>
虽然CDN方法非常简单,但你可能要考虑不在生产环境中使用CDN。
下载 htmx 副本
安装 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.3
Webpack
如果你使用 webpack 来管理你的 javascript :
-
通过你最喜欢的包管理器安装 htmx (例如 npm 或 yarm )
-
将 import 语句添加到你的 index.js 文件
import 'htmx.org';
如果要使用全局 htmx 变量(推荐),则需要将其注入到 window 范围:
-
创建自定义 JS 文件
-
将此文件导入到你的 index.js (位于步骤 2 的 import 语句下方)
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 To Messages
</button>
这告诉浏览器:
当用户点击此按钮时,向 URL /messages 发出 PUT 请求,并将响应加载到按钮中
触发请求
默认情况下,AJAX 请求由元素的“自然”事件触发: - input,textarea,select 在 change 事件发生时触发 - form 由 submit 事件触发 - 其余一切都由 click 事件触发
如果你想要不同的行为,你可以使用 hx-trigger 属性来指定哪个事件将触发请求。
以下代码中当鼠标进入 div 时,会向 /mouse_entered 发送 POST 请求:
<div hx-post="/mouse_entered" hx-trigger="mouseenter">
[Here Mouse, Mouse!]
</div>
触发修改器
触发器还可以包含一些额外的修饰符来改变其行为。例如:如果你希望请求仅发生一次,则可以对触发器使用修饰符 once :
<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
[Here Mouse, Mouse!]
</div>
可以用于触发器的其他修饰符有:
- changed - 仅当元素的值发生变化时才发出请求
- delay:<time interval> - 等待给定的时间量(例如1s)后再发出请求。如果事件再次触发,则倒计时将重置。
- throttle:<time interval> - 在发出请求之前等待给定的时间(例如 1 秒)。与 delay 不同,如果在达到时间限制之前发生新事件,则事件将被丢弃,因此请求将在时间段结束时触发。
- from:<CSS Selector> - 监听不同元素上的事件。这可以用于键盘快捷键之类的操作。
你可以使用这些属性来实现许多常见的 UX 模式,例如 Active Search:
<input type="text" name="q"
hx-get="/trigger_delay"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
placeholder="Search..."
>
<div id="search-results"></div>
如果输入已更改,则此输入将在按键事件发生后 500 毫秒发出请求,并将结果插入到 id 为 search-results 的 div 中。 可以在 hx-trigger 属性中指定多个触发器,以逗号分隔。
触发过滤器
你还可以通过在事件名称后使用方括号来应用触发器过滤器,并将要计算的 javascript 表达式括起来。如果表达式的计算结果为 true,则事件将触发,否则不会触发。
下面是一个仅在元素的 Control-Click 时触发的示例:
<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 中使用 htmx-indicator 类来完成此操作。
定义 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将添加类,从而显示旋转器 gif 元素。(最近我喜欢SVG 旋转器。)
虽然htmx-indicator该类使用不透明度来隐藏和显示进度指示器,但如果你更喜欢其他机制,你可以创建自己的 CSS 转换,如下所示:
这里我们有一个按钮。单击后,将向其添加 htmx-request 类,这将显示旋转 gif 元素。(我现在喜欢 SVG 旋转)。
虽然 htmx-indicator 类使用 opacity 来隐藏和显示进度指示器,但如果您更喜欢其他机制,您可以创建自己的 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 选择器。回顾我们的 Live Search 示例:
<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 ,而不是 input 标签中。
扩展 CSS 选择器
hx-target 和大多数采用 CSS 选择器的属性都支持“扩展”CSS 语法:
- 你可以使用this关键字,它表示属性所在的元素 hx-target 是目标
- 最接近的 <CSS selector>语法将找到 与给定的 CSS 选择器匹配的最近的父元素或其自身。(例如:最近的 tr 将定位到离元素最近的表格行)
- 该next <CSS selector>语法将在 DOM 中找到与给定 CSS 选择器匹配的下一个元素。
- 该previous <CSS selector>语法将在 DOM 中查找给定 CSS 选择器的前一个元素。 find <CSS selector>它将找到与给定 CSS 选择器匹配的第一个子元素。(例如:find tr 将定位到第一个子行元素)
此外,CSS 选择器可以包装在 < 和 /> 字符中,以模拟超媒体文本的查询文字语法。
像这样的相对目标对于创建灵活的用户界面很有用,而无需在 DOM 中添加大量id属性。
交换
htmx 提供了几种不同的方法来交换返回到 DOM 中的 HTML。默认情况下,内容会替换目标元素的 innerHTML 。你可以使用 hx-swap 属性修改此设置,其值为以下任意值:
名称 | 描述 |
---|---|
innerHTML | 默认值,将内容放在目标元素内 |
outerHTML | 用返回的内容替换整个目标元素 |
afterbegin | 将内容添加到目标中第一个子元素之前 |
beforebegin | 将内容添加到目标父元素中的目标之前 |
beforeend | 将内容附加到目标中的最后一个子项之后 |
afterend | 将内容附加到目标父元素中的目标之后 |
delete | 无论响应如何,都会删除目标元素 |
none | 不附加来自响应的内容(带外交换和响应头仍将被处理) |
变形交换
除了上述标准交换机制外,htmx 还支持通过扩展进行变形交换。Morphing swaps 尝试将新内容合并到现有 DOM 中,而不是简单地替换它。它们通常在交换操作期间就地改变现有节点,以牺牲更多 CPU 为代价,从而更好地保留焦点、视频状态等内容。
以下扩展可用于变形样式交换:
- Idiomorph — 由 htmx 开发人员创建的变形算法。
- Morphdom Swap - 基于morphdom,原始 DOM 变形库。
- Alpine-morph - 基于alpine morph插件,与 alpine.js 配合良好
视图转换
新的实验性 View Transitions API 为开发人员提供了一种在不同 DOM 状态之间创建动画过渡的方法。它仍处于积极开发阶段,并非在所有浏览器中都可用,但 htmx 提供了一种使用这个新 API 的方法,如果 API 在给定浏览器中不可用,则回退到非转换机制。
你可以使用以下方法试验这个新的 API:
- 将配置 htmx.config.globalViewTransitions 变量设置为 true 对所有交换使用转换
- 在属性 hx-swap 中使用 transition:true 选项
- 如果由于上述任一配置而导致元素交换转换,你可以捕获该 htmx:beforeTransition 事件并调用 preventDefault() 来取消它的转换。
如 Chrome 文档中有关该功能所述,可以使用 CSS 配置视图转换。
你可以在动画示例页面上看到视图转换示例。
交换选项
hx-swap 属性支持许多选项来调整 htmx 的交换行为。例如,默认情况下,htmx 将在找到的标题标签的标题中任何位置交换新内容。您可以通过将 ignoreTitle 修饰符设置为 true 来关闭此行为:
<button hx-post="/like" hx-swap="outerHTML ignoreTitle:true">Like</button>
可用的修饰符hx-swap有:
选项 | 描述 |
---|---|
transition | true 或者 false,是否使用视图转换 API 进行此交换 |
swap | 在清除旧内容和插入新内容之间使用的交换延迟(例如100ms) |
settle | 在插入新内容和固定新内容之间使用的结算延迟(例如100ms) |
ignoreTitle | 如果设置为true,则新内容中找到的任何标题都将被忽略,并且不会更新文档标题 |
scroll | top 或 bottom,将目标元素滚动到其顶部或底部 |
show | top 或者 bottom,将目标元素滚动到视图的顶部或底部 |
所有交换修饰符在指定交换样式后出现,并以英文冒号分隔。
有关这些选项的更多详细信息,请参阅 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 还支持以 javascript 编程方式取消请求:你可以将 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 可以轻松使用CSS Transitions,无需 JavaScript。参考以下 HTML 内容:
<div id="div1">Original Content</div>
想象一下,这个内容被 ajax 或 htmx 替换成新的内容:
<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 属性匹配的元素。如果在新内容中找到元素的匹配项,则会在交换发生之前将旧内容的属性复制到新元素上。然后换入新内容,但使用旧的属性值。最后,在 “settle” 延迟 (默认为 20ms) 之后换入新的属性值。有点疯狂,但这就是允许 CSS 过渡在开发人员没有任何 javascript 的情况下工作的原因。
带外交换(Out of Band Swaps)
如果你想使用 id 属性将响应中的内容直接交换到 DOM,你可以在响应 html 中使用 hx-swap-oob 属性:
<div id="message" hx-swap-oob="true">Swap me directly!</div>
Additional Content
在此响应中,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 表单一样,input 的 name 属性作为 htmx 发送的请求中的参数名称。
此外,如果元素发生非 GET 请求,则将包括最近表单的所有输入的值。
如果你希望包含其他元素的值,则可以将 hx-include 属性与 CSS 选择器结合使用,选择你想要在请求中包含其值的所有元素。
如果你希望过滤掉一些参数,你可以使用 hx-params 属性。
最后,如果你想以编程方式修改参数,你可以使用 htmx:configRequest 事件。
文件上传
如果你希望通过 htmx 请求上传文件,可以将 hx-encoding 属性设置为 multipart/form-data。这将使用一个 FormData 对象来提交请求,并将文件正确地包含在请求中。
请注意,根据你的服务器端技术,你可能必须以不同的方式处理具有此类正文内容的请求。
请注意,htmx 触发 htmx:xhr:progress 事件时会根据上传过程中的标准事件定期触发 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 属性的元素上),可用于实现请求的异步确认。
下面是对带有 confirm-with-sweet-alert='true' 属性的任何元素使用 sweet alert 的示例:
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 支持使用 hx-boost 属性“提升”常规 HTML 锚点和表单。此属性会将所有锚点标签和表单转换为 AJAX 请求,默认情况下,这些请求会以页面主体为交换目标。
以下是一个例子:
<div hx-boost="true">
<a href="/blog">Blog</a>
</div>
此 div 中的锚标记将向 /blog 发出 AJAX GET 请求并将响应交换到 body 标记中。
渐进增强
hx-boost 的一个特点是,如果不启用 javascript,它会优雅地降级:链接和表单继续工作,它们只是不使用 ajax 请求。这被称为渐进式增强,它允许更广泛的受众使用你网站的功能。
其他 htmx 模式也可以进行调整以实现渐进增强,但这需要更多的考虑。
参考 active search 示例。正如它所写的那样,它不会优雅地降级:没有启用 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 的客户端将能够按下 Enter 键并继续搜索。更好的是,你还可以添加“搜索”按钮。然后,你需要使用 hx-post 属性的镜像更新表单,或者在其 action 上使用 hx-boost 。
你需要在服务器端检查 HX-Request 头,以区分 htmx 驱动和常规请求,从而确定向客户端呈现的具体内容。
其他模式也可以进行类似调整,以满足应用程序的渐进增强需求。
正如你所见,这需要更多的考虑和工作。它还规定了一些完全超出范围的功能。这些必须由开发人员根据项目目标和受众做出权衡。
可访问性是一个与渐进增强密切相关的概念。使用渐进增强技术(例如hx-boost将使你的 htmx 应用程序更易于广大用户访问)。
基于 htmx 的应用程序与普通的非 AJAX 驱动的 Web 应用程序非常相似,因为 htmx 是面向 HTML 的。
因此,适用常规 HTML 可访问性建议如下:
- 尽可能使用语义 HTML(即为正确的事物使用正确的标签)
- 确保焦点状态清晰可见
- 将文本标签与所有表单字段关联
- 使用适当的字体、对比度等最大限度地提高应用程序的可读性。
历史支持
htmx 提供了一种与浏览器历史记录 API 交互的简单机制:
如果你希望给定元素将其请求 URL 推送到浏览器导航栏并将页面的当前状态添加到浏览器的历史记录中,请包含hx-push-url属性: 如果你希望将元素请求的 URL 推送到浏览器导航栏,并将页面的当前状态添加到浏览器的历史记录中,请使用 hx-push-url 属性:
<a hx-get="/blog" hx-push-url="true">Blog</a>
当用户点击此链接时,htmx 将快照当前 DOM 并将其存储,然后再向 /blog 发出请求。然后,它会进行交换并将新位置推送到历史堆栈上。
当用户点击后退按钮时,htmx 将从存储中检索旧内容并将其交换回目标,模拟“返回”到之前的状态。如果在缓存中找不到该位置,htmx 将向给定的 URL 发出 ajax 请求,并将HTTP头 HX-History-Restore-Request 设置为 true,并期望返回整个页面所需的 HTML。或者,如果 htmx.config.refreshOnHistoryMiss 配置变量设置为 true,它将发出浏览器硬刷新请求。
注意: 如果你将 URL 推送到历史记录中,则必须能够导航到该 URL 并返回完整页面!用户可以将 URL 复制并粘贴到电子邮件或新选项卡中。此外,如果页面不在历史记录缓存中,则 htmx 在恢复历史记录时将需要整个页面。
指定历史快照元素
默认情况下,htmx 将使用 body 来获取和恢复历史快照。这通常是正确的做法,但如果你想使用指定的元素来获取快照,则可以使用 hx-history-elt 属性来指定其他元素。
小心:此元素需要出现在所有页面上,否则从历史记录中恢复将无法可靠地进行。
通过第三方库撤销 DOM 变更
如果你使用的是第三方库并希望使用 htmx 历史记录功能,则需要在拍摄快照之前清理 DOM。让我们参考一下 Tom Select 库,它使 select 元素的用户体验更加丰富。让我们设置 TomSelect 将任何具有 .tomselect 类的输入元素转换为丰富的 select 元素。
首先我们需要在新内容中初始化具有该类的元素:
htmx.onLoad(function (target) {
// find all elements in the new content that should be
// an editor and init w/ TomSelect
var editors = target.querySelectorAll(".tomselect")
.forEach(elt => new TomSelect(elt))
});
这将为所有具有该类 .tomselect 的输入元素创建一个丰富的选择器 。但是,它会改变 DOM,我们不希望将该改变保存到历史记录缓存中,因为当历史记录内容重新加载到屏幕时,TomSelect 将被重新初始化。
为了解决这个问题,我们需要捕获 htmx:beforeHistorySave 事件,并通过对它们调用 destroy() 来清除 TomSelect 突变:
htmx.on('htmx:beforeHistorySave', function() {
// find all TomSelect elements
document.querySelectorAll('.tomSelect')
.forEach(elt => elt.tomselect.destroy()) // and call destroy() on them
})
这会将 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 的上述行为。此对象是Json格式,定义示例如下:
responseHandling: [
{code:"204", swap: false}, // 204 - No Content by default does nothing, but is not an error
{code:"[23]..", swap: true}, // 200 & 300 responses are non-errors and are swapped
{code:"[45]..", swap: false, error:true}, // 400 & 500 responses are not swapped and are errors
{code:"...", swap: false} // catch all for any other response code
]
当 htmx 收到响应时,它将按顺序迭代 htmx.config.responseHandling 数组,并测试给定对象的 code 属性(被视为正则表达式时)是否与当前响应匹配。如果条目与当前响应代码匹配,则将用于确定是否以及如何处理响应。
此数组中的响应处理配置可用的字段包括:
- code - 一个代表将根据响应代码进行测试的正则表达式的字符串。
- swap - 如果为 true 响应应该被交换到 DOM 中,否则设为 false
- error - 如果为 true htmx 则将此响应视为错误
- ignoreTitle - 如果 htmx 应忽略响应中的标题标签则设为 true
- select - 用于从响应中选择内容的 CSS 选择器
- target - 指定响应的替代目标的 CSS 选择器
- swapOverride - 响应的替代交换机制
配置响应处理示例
作为如何使用此配置的示例,请考虑服务器端框架在发生验证错误时以 422 - Unprocessable Entity 响应进行响应的情况。默认情况下,htmx 将忽略响应,因为它与正则表达式 [45]...
使用 meta 机制配置 responseHandling,我们可以添加以下配置:
<!--
* 204 No Content by default does nothing, but is not an error
* 2xx, 3xx and 422 responses are non-errors and are swapped
* 4xx & 5xx responses are not swapped and are errors
* all other responses are swapped using "..." as a catch-all
-->
<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}]}' /> <!--all responses are swapped-->
最后,值得考虑使用 response extension ,它允许你通过属性以声明方式配置响应代码的行为。
CORS
在跨源上下文中使用 htmx 时,请记住配置你的 Web 服务器以设置访问控制头,以便 htmx 头在客户端可见。
- Access-Control-Allow-Headers(用于请求头)
- Access-Control-Expose-Headers(用于响应头)
请求头
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 - 允许你在 Settle 步骤后触发客户端事件
- HX-Trigger-After-Swap - 允许你在交换步骤后触发客户端事件
有关 HX-Trigger 标头的更多信息,请参阅 HX-Trigger 响应头。
通过 htmx 提交表单的好处是不再需要 Post/Redirect/Get 模式。在服务器上成功处理 POST 请求后,你不需要返回HTTP 302 (Redirect)。你可以直接返回新的 HTML 片段。
此外,上述响应头不会提供给 htmx 进行处理,例如HTTP 302(重定向)等 3xx 重定向响应代码。相反,浏览器将在内部拦截重定向并从重定向的 URL 返回标头和响应。尽可能使用替代响应代码(例如 200)来允许返回这些响应头。
请求操作顺序
htmx 请求中的操作顺序如下:
- 元素被触发并开始请求
- 为请求收集值
- 该 htmx-request 类应用于适当的元素
- 然后通过 AJAX 异步发出请求
- 收到响应后,目标元素将被标记为 htmx-swapping 类
- 应用可选的交换延迟(参见 hx-swap 属性)
- 实际内容交换已完成
- 该 htmx-swapping 类已从目标中移除
- htmx-added 每条新内容都会添加相应的类别
- 该 htmx-settling 类应用于目标
- 完成延迟(默认值:20ms)
- DOM 已稳定
- 该 htmx-settling 类已从目标中移除
- htmx-added 每次新增内容时都会删除该类 你可以使用 htmx-swapping 和 htmx-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 事件并要求输入具有以下值的输入示例:
<form id="example-form" hx-post="/test">
<input name="example"
onkeyup="this.setCustomValidity('') // reset the validation on keyup"
hx-on:htmx:validation:validate="if(this.value != 'foo') {
this.setCustomValidity('Please enter the value foo') // set the validation error
htmx.find('#example-form').reportValidity() // report the issue
}">
</form>
请注意,所有客户端验证都必须在服务器端重新完成,因为它们总是可以被绕过。
扩展
htmx 提供了一种扩展机制,允许你自定义库的行为。扩展在 javascript 中定义,然后通过 hx-ext 属性启用。
以下是安装 idiomorph 扩展的方法,它允许你使用 Idiomorph DOM 变形算法进行 htmx 交换:
<head>
<script src="https://unpkg.com/idiomorph@0.3.0/dist/idiomorph-ext.min.js"></script>
</head>
<body hx-ext="morph">
...
<button hx-post="/example" hx-swap="morph" hx-target="#content">
Update Content
</button>
...
</body>
首先包含扩展名(应该在 htmx.js 之后包含),然后通过 hx-ext 属性按名称引用扩展名。这使您能够使用 morph swap。
核心扩展
htmx 支持一些“核心”扩展,这些扩展由 htmx 开发团队支持:
- head-support - 支持在 htmx 请求中合并 head 标签信息(样式等)
- htmx-1-compat - 恢复 htmx 1 默认值和功能
- idiomorph - 支持 morph 使用 idiomorph 的交换策略
- 预加载 - 允许你预加载内容以获得更好的性能
- response-targets - 允许你根据 HTTP 响应代码定位元素(例如404)
- sse - 支持服务器发送事件
- ws — 支持 Web Sockets 你可以在扩展页面上查看所有可用的扩展。
创建扩展
如果你有兴趣向 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(); // add a new parameter into the request
evt.detail.headers['Authentication-Token'] = getAuthToken(); // add a new header into the request
});
这里我们在发送请求之前添加一个参数和头。
使用事件修改交换行为
你可以处理该 htmx:beforeSwap 事件以修改 htmx 的交换行为:
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if(evt.detail.xhr.status === 404){
// alert the user when a 404 occurs (maybe use a nicer mechanism than alert())
alert("Error: Could Not Find Resource");
} else if(evt.detail.xhr.status === 422){
// allow 422 responses to swap as we are using this as a signal that
// a form was submitted with bad data and want to rerender with the
// errors
//
// set isError to false to avoid error logging in console
evt.detail.shouldSwap = true;
evt.detail.isError = false;
} else if(evt.detail.xhr.status === 418){
// if the response code 418 (I'm a teapot) is returned, retarget the
// content of the response to the element with the id `teapot`
evt.detail.shouldSwap = true;
evt.detail.target = htmx.find("#teapot");
}
});
这里我们处理一些 400 级错误响应代码 ,这些代码通常不会在 htmx 中进行交换。
事件命名
请注意,所有事件都以两个不同的名称触发
- 驼峰式命名法
- 烤肉串案例
例如,你可以监听 htmx:afterSwap 或 htmx: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 到控制台,并允许你准确地看到正在发生的事情。
请注意,这只能在控制台中使用,你不能将其嵌入到页面的脚本标签中。
最后,无论如何,你可能只想通过加载未最小化的版本进行调试。它大约有 2500 行 JavaScript 代码,因此代码量并不大。你很可能想在 htmx.js issueAjaxRequest() 和 handleAjaxResponse() 方法 中设置一个断点来查看发生了什么。
如果你需要帮助,请随时加入 htmx 官方讨论组。
创建演示
有时,为了演示错误或阐明用法,最好能够使用像 jsfiddle 这样的 JavaScript 代码片段。为了方便创建演示,htmx 托管了一个演示脚本站点:
- 页面
- 超脚本
- 请求模拟库
只需将以下脚本标签添加到你的 demo/fiddle/whatever 中:
<script src="https://demo.htmx.org"></script>
此帮助程序允许您通过添加带有 url 属性的模板标签来添加模拟响应,以指示哪个 URL。该 url 的响应将是模板的 innerHTML,从而可以轻松构建模拟响应。您可以使用 delay 属性向响应添加延迟,该属性应为整数,指示延迟的毫秒数。
你可以使用 ${} 语法在模板中嵌入简单的表达式。
请注意,这只应用于演示,并且不能保证长时间工作,因为它将始终抓取最新版本的 htmx 和 hyperscript!
演示示例
以下是代码运行的示例:
<!-- load demo environment -->
<script src="https://demo.htmx.org"></script>
<!-- post to /foo -->
<button hx-post="/foo" hx-target="#result">
Count Up
</button>
<output id="result"></output>
<!-- respond to /foo with some dynamic content in a template tag -->
<script>
globalInt = 0;
</script>
<template url="/foo" delay="500"> <!-- note the url and delay attributes -->
${globalInt++}
</template>
脚本
虽然 htmx 鼓励使用超媒体方法来构建 Web 应用程序,但它也为客户端脚本提供了许多选项。脚本包含在 REST-ful Web 架构描述中,请参阅:Code-On-Demand。我们建议在可行的情况下,在 Web 应用程序中采用超媒体友好的脚本方法:
- 尊重 HATEOAS
- 使用事件在组件之间进行通信
- 使用孤岛将非超媒体组件与应用程序的其余部分隔离开来
- 考虑内联脚本
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('You clicked me!')">
Click Me!
</button>
此功能允许脚本逻辑与逻辑适用的 HTML 元素位于同一位置,从而提供良好的行为局部性 (LoB)。不幸的是,HTML 只允许固定数量的特定 DOM 事件(例如 onclick)的 on* 属性,并且没有提供响应元素上任意事件的通用机制。
为了解决这一缺点,htmx 提供了 hx-on 属性。这些属性允许你以保留标准属性的 LoB 的方式响应任何 on 事件。
如果我们想使用 hx-on 属性来响应单击事件,我们可以这样写:
<button hx-on:click="alert('You clicked me!')">
Click Me!
</button>
用法是字符串 hx-on 后跟冒号(或破折号),然后是事件的名称。
当然,对于单击事件,我们建议坚持使用标准 onclick 属性。但是,考虑一个希望使用 on* 事件向请求添加参数的 htmx 驱动按钮 htmx:config-request 。使用标准属性无法做到这一点,但可以使用 hx-on:htmx:config-request 属性来完成:
<button hx-post="/example"
hx-on:htmx:config-request="event.detail.parameters.example = 'Hello Scripting!'">
Post Me!
</button>
此处,example 参数在发出请求之前添加到 POST 请求中,其值为 “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">Updating...</div>
<div><input type='hidden' name='item' value='1'/>Item 1</div>
<div><input type='hidden' name='item' value='2'/>Item 2</div>
<div><input type='hidden' name='item' value='2'/>Item 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 向具有 htmx 属性的 DOM 中添加内容,则需要确保使用该 htmx.process() 函数初始化该内容。
例如,如果你要使用 fetchAPI 获取一些数据并将其放入 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">Toggle New Content</button>
<template x-if="show_new">
<div id="new_content">
<a hx-get="/server/newstuff" href="#">New Clickable</a>
</div>
</template>
</div>
Web 组件
请参阅Web 组件示例页面,获取有关如何集成 htmx 与 Web 组件的示例。
缓存
htmx 可以与标准 HTTP 缓存 机制配合使用。
如果你的服务器将 Last-Modified HTTP 响应头添加到给定 URL 的响应中,则浏览器会自动将 If-Modified-Since 请求 HTTP 头添加到对同一 URL 的下一个请求中。请注意,如果您的服务器可以根据其他一些头为同一 URL 呈现不同的内容,则需要使用 Vary 响应 HTTP 头。例如,如果你的服务器在 HX-Request 标头缺失或 false 时呈现完整的 HTML,并且在 HX-Request:true 时呈现该 HTML 的片段,则需要添加 Vary: HX-Request。这会导致缓存根据响应 URL 和 HX-Request 请求头的组合进行键入,而不仅仅是基于响应 URL。
如果您无法(或不愿意)使用 Vary 标头,则可以将配置参数 getCacheBusterParam 设置为 true。如果设置了此配置变量,则 htmx 将在它发出的 GET 请求中包含一个 cache-busting 参数,这将阻止浏览器将基于 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 内容(再次强调,不建议这样做!),那么你可以在内容周围放置一个带有属性的 div hx-disable:
<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避免在缓存中存储任何 HTMLlocalStorage
- htmx.config.allowEval - 可以设置为 false 禁用所有依赖于 eval 的 htmx 功能:
- 事件过滤器
- hx-on:属性
- hx-vals带有js:前缀
- hx-headers带有js:前缀
请注意,所有通过禁用删除的功能都 eval() 可以使用你自己的自定义 javascript 和 htmx 事件模型重新实现。
事件
如果你希望允许对当前主机之外的某些域的请求,但又不完全开放,则可以使用事件 htmx:validateUrl 。此事件将在槽中提供请求 URLdetail.url 以及 sameHost 属性。
你可以检查这些值,如果请求无效,则调用 preventDefault() 事件以阻止发出请求。
document.body.addEventListener('htmx:validateUrl', function (evt) {
// only allow requests to the current server as well as myserver.com
if (!evt.detail.sameHost && evt.detail.url.hostname !== "myserver.com") {
evt.preventDefault();
}
});
CSP 选项
浏览器还提供进一步保护 Web 应用程序的工具。最强大的工具是 内容安全策略。使用 CSP,你可以告诉浏览器不要向非源主机发出请求、不要评估内联脚本标签等。
以下是标签中的 CSP 示例meta:
<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,如果 htmx 设置为true,将在历史记录未命中时发出整页刷新,而不是使用 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,允许使用诸如 cookies、授权头或 TLS 客户端证书之类的凭证进行跨站点访问控制请求 |
htmx.config.timeout | 默认为 0,即请求在自动终止前可以花费的毫秒数 |
htmx.config.scrollBehavior | 默认为 'instant',即将 show 修饰符与 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 将在交换新内容时使用 View Transition API。 |
htmx.config.methodsThatUseUrlParams | 默认为["get", "delete"],htmx 将通过在 URL 中编码其参数来格式化使用这些方法的请求,而不是请求正文 |
htmx.config.selfRequestsOnly | 默认为true,是否仅允许对与当前文档相同的域进行 AJAX 请求 |
htmx.config.ignoreTitle | 默认为 false,如果设置为 true,则 htmx 在新内容中找到 title 标签时不会更新文档的标题 |
htmx.config.disableInheritance | hx-inherit 禁用 htmx 中的属性继承,然后可以通过属性覆盖该继承 |
htmx.config.scrollIntoViewOnBoost | 默认为true,无论增强元素的目标是否滚动到视口中。如果在增强元素上省略 hx-target ,则目标默认为body,导致页面滚动到顶部。 |
htmx.config.triggerSpecsCache | 默认为null,用于存储评估的触发器规范的缓存,以更多的内存使用为代价来提高解析性能。你可以定义一个简单的对象来使用永不清除的缓存,或者使用代理对象实现你自己的系统 |
htmx.config.responseHandling | 可以在此处配置响应状态代码的默认响应处理行为,以交换或错误 |
htmx.config.allowNestedOobSwaps | 默认为true,是否处理嵌套在主响应元素中的元素的 OOB 交换。请参阅嵌套 OOB 交换。 |
你可以直接在 javascript 中设置它们,也可以使用meta标签:
<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>