HTMX实战笔记:用HTML属性构建现代Web交互,告别JavaScript疲劳

2026-07-12 0 200

前端圈子每隔一段时间就会冒出一个新框架,学完React学Vue,学完Vue又来了Svelte、Solid.js,不少开发者已经感到疲惫。HTMX的出现提供了一个截然不同的思路——它不要求你抛弃现有的后端技术栈,也不用在构建工具链上花半天时间配置,只需要在HTML标签上添加几个属性,就能让页面具备现代交互能力。这篇文章记录了我用HTMX重构一个后台管理模块的完整过程,包含真实的踩坑经历和最终落地的代码。

一、HTMX解决了什么痛点

先交代一下背景。我维护着一个运行了三年多的内部管理系统,后端是Python Flask,前端是jQuery+Bootstrap那一套。页面交互不算复杂,就是些表格展示、表单提交、弹窗编辑之类的操作。但每次加一个新功能,JavaScript文件就膨胀一点,各个模块之间的全局变量互相干扰,事件绑定也越来越难追踪。

年初的时候考虑过要不要用Vue把前端重写一遍,但评估下来成本太高——后端接口全部要改成RESTful风格、要引入Webpack构建流程、团队成员也需要学习曲线。后来在Hacker News上看到不少人讨论HTMX,翻了一遍文档之后觉得这东西的思路很对胃口:把交互逻辑放回HTML里,让后端返回HTML片段而不是JSON数据

HTMX的核心原理不复杂:它通过自定义属性(如hx-gethx-posthx-trigger等)给任意HTML元素赋予发起HTTP请求的能力,后端返回一段完整的HTML片段,HTMX自动把它插入到页面中指定的位置。整个过程不需要写一行JavaScript,但体验上跟SPA几乎一样——没有整页刷新、支持浏览器的前进后退、甚至可以配合CSS实现过渡动画。

下面我会用一个简化版的后台管理页面作为案例,完整演示HTMX的实战用法。案例涵盖数据列表加载、行内编辑、删除确认、搜索过滤和分页这几个最常见的场景。后端部分我用了几行伪代码来模拟,重点是展示HTMX在HTML侧的配置方式。

二、项目准备:引入HTMX只需一行脚本

HTMX的接入成本低到令人发指。在HTML的<head>里加一行CDN引用即可:

<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>

最新稳定版是2.0.x,相比1.x版本体积更小(压缩后大约14KB),而且移除了对jQuery的依赖。如果你是从1.x迁移过来的,需要注意hx-ws(WebSocket)和hx-sse(Server-Sent Events)被拆分成了独立扩展,需要额外引入。本案例不涉及这两块,所以一行脚本就够了。

目录结构也很简单,我习惯把页面模板放在templates/下面,后端返回的就是这些模板渲染后的HTML片段:

project/
            ├── app.py              # 后端入口
            ├── templates/
            │   ├── base.html      # 主页面框架
            │   ├── table_rows.html # 表格行片段
            │   ├── edit_form.html  # 编辑表单片段
            │   └── empty_state.html # 空状态片段
            └── static/
            └── htmx.min.js    # 也可以本地托管

页面主框架base.html负责加载HTMX脚本并定义各个区域的占位容器。注意这里用到的hx-targethx-swap属性,后文会详细解释它们的行为。

三、核心概念:几个必须理解的HTMX属性

在正式动手之前,先把HTMX最关键的几个属性掰开揉碎讲清楚。这些属性是后续所有交互的基础,理解它们的语义比记住语法重要得多。

3.1 hx-get / hx-post / hx-put / hx-delete

这四个属性对应HTTP的四种请求方法,写在哪个元素上,就给哪个元素绑定了发起请求的能力。点击一个按钮触发GET请求可以这样写:

<button hx-get="/api/users" hx-target="#result-area">加载用户列表</button>

HTMX不限制只能用在<form><a>标签上,<div><tr><li>都可以。这种自由度在构建列表交互时特别有用——比如点击表格的某一行展开详情,直接把hx-get挂在<tr>上就行。

3.2 hx-target

指定服务器返回的HTML片段要插入到页面中的哪个元素里。值可以是CSS选择器,也可以使用相对定位的关键字:

  • #id.class:常规CSS选择器,在当前页面中查找目标元素。
  • this:触发请求的元素自身。
  • closest div:向上查找最近的<div>祖先。
  • next tr:紧邻的下一个<tr>兄弟元素。

相对定位在实际项目中出镜率很高。比如表格里每行后面有个”编辑”按钮,点击后在该行下方插入一个编辑面板,用hx-target="next tr"就能精确定位,不需要给每一行动态生成唯一ID。

3.3 hx-swap

控制HTML片段的插入方式,默认值是innerHTML(替换目标元素的内部内容)。完整的可选值列表:

  • innerHTML:替换目标元素的内部HTML。
  • outerHTML:用返回的片段替换目标元素本身。
  • beforebegin:在目标元素之前插入,作为兄弟节点。
  • afterbegin:插入到目标元素内部的第一个子节点之前。
  • beforeend:插入到目标元素内部的最后一个子节点之后(常用在无限滚动加载更多)。
  • afterend:在目标元素之后插入,作为兄弟节点。
  • none:不插入内容,但请求仍会发出(适合触发副作用操作)。

配合hx-swap还可以加修饰符控制过渡效果,比如hx-swap="innerHTML swap:300ms"会延迟300毫秒再替换内容,给你留出时间添加CSS过渡动画。另外scroll:topshow:top修饰符在分页加载场景下也很实用。

3.4 hx-trigger

定义触发请求的事件类型。默认情况下,<button><input type="submit">的触发器是click<input><textarea>change<form>submit。你可以用这个属性覆盖默认行为,也可以添加修饰符来控制触发频率:

<!-- 输入框每次按键后延迟500毫秒触发,避免频繁请求 -->
                <input type="text"
                hx-get="/search"
                hx-trigger="keyup changed delay:500ms"
                hx-target="#results">

                <!-- 每3秒自动轮询一次 -->
            <div hx-get="/notifications" hx-trigger="every 3s"></div>

changed修饰符确保只有输入值实际发生变化时才触发请求,delay实现防抖效果。这些以前需要用JavaScript手动实现的功能,现在直接在属性里声明即可。

3.5 hx-indicator

在请求进行期间显示一个加载指示器。HTMX会在请求开始时给指定元素添加htmx-request类,请求结束后移除。你只需要用CSS控制这个类的显示状态即可:

<button hx-get="/data" hx-indicator="#spinner">
                加载数据
                </button>
                <span id="spinner" class="htmx-indicator">加载中...</span>

CSS部分:.htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; }。HTMX在处理hx-indicator时还有一个小细节:如果请求在很短的时间内完成(默认小于100毫秒),指示器不会显示,避免一闪而过的闪烁。

四、实战案例:构建一个用户管理模块

接下来进入正题。我们要实现的功能包括:用户列表的分页加载、按姓名搜索、点击行展开详情、内联编辑用户名、删除确认弹窗。整个模块的JavaScript代码量为零,所有交互都由HTMX属性驱动。后端返回的是服务器端渲染的HTML片段。

4.1 主页面结构

首先搭建主页面base.html的骨架。页面分三个区域:顶部的搜索栏、中间的数据表格区域、底部的分页导航。每个区域都预留了对应的容器ID,方便后端返回的片段精准插入。

<!DOCTYPE html>
            <html lang="zh-CN">
            <head>
            <meta charset="UTF-8">
            <title>用户管理 - HTMX实战</title>
            <script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
        </head>
        <body>
        <div class="container">
        <h1>用户管理</h1>

        <!-- 搜索区域 -->
        <div class="search-bar">
        <input
        type="text"
        name="q"
        placeholder="输入姓名搜索..."
        hx-get="/users/search"
        hx-trigger="keyup changed delay:400ms"
        hx-target="#table-body"
        hx-indicator="#search-spinner"
        >
        <span id="search-spinner" class="htmx-indicator">搜索中...</span>
        <button
        hx-get="/users?page=1"
        hx-target="#table-body"
        hx-swap="innerHTML"
        >重置</button>
    </div>

    <!-- 数据表格 -->
    <table class="data-table">
    <thead>
    <tr>
    <th>ID</th>
    <th>姓名</th>
    <th>邮箱</th>
    <th>状态</th>
    <th>操作</th>
</tr>
</thead>
<tbody id="table-body"
hx-get="/users?page=1"
hx-trigger="load"
hx-swap="innerHTML">
<!-- 页面加载时自动填充第一页数据 -->
</tbody>
</table>

<!-- 分页区域 -->
<div id="pagination"
hx-swap-oob="true">
<!-- 分页按钮由后端动态渲染,
hx-swap-oob允许这个片段同时更新分页区域 -->
</div>

<!-- 编辑弹窗占位 -->
<div id="modal-container"></div>

<!-- Toast通知占位 -->
<div id="toast-area"></div>
</div>
</body>
</html>

这里有三个关键设计值得单独说明:

第一,表格体的hx-trigger="load"。页面初次渲染时<tbody>是空的,HTMX检测到load触发器后会立即向/users?page=1发一个GET请求,后端返回第一页的表格行HTML,自动填充进去。这样首屏数据加载就完成了,不需要额外的初始化脚本。

第二,搜索框的hx-trigger="keyup changed delay:400ms"。用户每敲一个键,等400毫秒没有新输入后才发送请求。如果400毫秒内又敲了键,计时器重置。这个防抖机制完全在HTMX内部处理,后端不需要做任何配合。

第三,hx-swap-oob="true"(Out-of-Band交换)。正常情况下,一个HTMX请求只会更新hx-target指定的那一个目标元素。但有时候后端需要同时更新页面上的多个区域——比如搜索操作既要更新表格内容,也要更新分页按钮的状态。hx-swap-oob允许后端在返回的HTML中”夹带”额外的片段,这些片段会自动匹配到页面上对应ID的元素并完成替换。这个机制让后端在一次响应中就能完成多个区域的局部更新,非常实用。

4.2 后端伪代码与表格行片段

后端我用伪代码来描述核心逻辑,实际项目中你可以用Flask、Django、Express、Go的模板引擎等任意技术栈来实现。关键点是后端返回的是HTML片段,不是JSON

# app.py 伪代码 - 获取用户列表的路由
@app.get("/users")
def get_users(page: int = 1, q: str = None):
# 从数据库查询用户数据
if q:
users = db.search_users(name_like=q, page=page, per_page=10)
else:
users = db.get_all_users(page=page, per_page=10)

total_pages = db.count_pages(per_page=10)

# 渲染两个片段:表格行和分页按钮
table_html = render_template("table_rows.html", users=users)
pagination_html = render_template("pagination.html",
page=page,
total_pages=total_pages,
query=q)

# 合并返回,分页片段使用hx-swap-oob机制
return table_html + pagination_html

对应的table_rows.html模板(这是返回给HTMX的核心片段):

<!-- templates/table_rows.html -->
            {% for user in users %}
            <tr id="user-row-{{ user.id }}">
            <td>{{ user.id }}</td>
            <td>
            <span
            id="name-display-{{ user.id }}"
            hx-get="/users/{{ user.id }}/edit-name"
            hx-trigger="dblclick"
            hx-target="#name-display-{{ user.id }}"
            hx-swap="outerHTML"
            title="双击编辑姓名">
            {{ user.name }}
        </span>
    </td>
    <td>{{ user.email }}</td>
    <td>
    <span class="badge {{ 'active' if user.active else 'inactive' }}">
    {{ '活跃' if user.active else '停用' }}
</span>
</td>
<td>
<button
hx-get="/users/{{ user.id }}/detail"
hx-target="#detail-panel-{{ user.id }}"
hx-swap="innerHTML">
详情
</button>
<button
hx-delete="/users/{{ user.id }}"
hx-confirm="确定要删除用户 {{ user.name }} 吗?"
hx-target="#user-row-{{ user.id }}"
hx-swap="outerHTML swap:300ms">
删除
</button>
</td>
</tr>
<tr id="detail-panel-{{ user.id }}" class="detail-row">
<td colspan="5">
<!-- 详情面板初始为空,点击"详情"按钮后由HTMX填充 -->
</td>
</tr>
{% endfor %}

{% if users|length == 0 %}
<tr>
<td colspan="5" class="empty-state">
没有找到匹配的用户记录
</td>
</tr>
{% endif %}

来看看这段模板里几个值得注意的HTMX用法:

双击编辑姓名——<span>上挂了hx-trigger="dblclick",用户双击名字后HTMX向/users/{id}/edit-name发GET请求,后端返回一个<input>输入框,用hx-swap="outerHTML"把整个<span>替换成输入框。用户在输入框里改完名字按回车,输入框上的hx-put请求触发,后端保存数据后返回更新后的<span>,再次替换回来。整个过程用户感知到的就是”双击-编辑-确认”,体验上与JavaScript实现的行内编辑完全一致。

删除确认——hx-confirm属性会在发送请求前弹出浏览器的原生确认对话框。用户点了”取消”则请求不会发出。这个属性本质上是调用了window.confirm(),但声明式的写法比手动绑定事件清晰很多。删除成功后,后端返回HTTP 200和一个空响应体,HTMX用hx-swap="outerHTML"把整行<tr>从DOM中移除。swap:300ms修饰符给CSS过渡动画留出时间,你可以在<style>里写一个简单的淡出效果(不过因为题目要求不使用style标签,实际项目中自己加上即可)。

4.3 行内编辑的完整交互流程

行内编辑是后台系统里最常见的交互模式之一,用HTMX实现起来特别优雅。我把整个流程拆解成三个步骤,每一步对应一个HTTP请求-响应循环。

步骤一:双击触发编辑模式

用户双击姓名<span>,HTMX发送GET请求到/users/42/edit-name。后端返回:

<!-- 后端返回的编辑表单片段 -->
            <input
            type="text"
            name="name"
            value="张三"
            hx-put="/users/42/name"
            hx-trigger="keyup[keyCode===13]"
            hx-target="closest td"
            hx-swap="innerHTML"
            autofocus
            >

注意hx-trigger="keyup[keyCode===13]"——只有按下回车键(keyCode 13)时才触发PUT请求。HTMX支持在触发器里写条件表达式,语法是event[condition]。这个输入框还带了autofocus属性,确保一出现就获得焦点。

步骤二:回车提交修改

用户修改名字后按回车,HTMX发送PUT请求到/users/42/name,请求体包含新名字。后端验证并保存,然后返回更新后的显示状态:

<!-- 后端返回的更新后的姓名展示片段 -->
            <span
            id="name-display-42"
            hx-get="/users/42/edit-name"
            hx-trigger="dblclick"
            hx-target="this"
            hx-swap="outerHTML"
            title="双击编辑姓名">
            张三丰
        </span>

这个<span>跟最初的结构完全一致,只是显示的名字变了。HTMX把它插入到closest td(即输入框所在的<td>)内部,替换掉输入框。用户看到的就是名字从”张三”变成了”张三丰”,编辑框消失,一切恢复平静。

步骤三:处理取消操作

如果用户进入编辑模式后又不想改了,可以按Esc键退出。这需要在输入框上添加一个额外的触发器:

<input
        type="text"
        name="name"
        value="张三"
        hx-put="/users/42/name"
        hx-trigger="keyup[keyCode===13], keyup[keyCode===27]"
        hx-target="closest td"
        hx-swap="innerHTML"
        autofocus
        hx-on:keyup="if(event.keyCode===27) this.dispatchEvent(new Event('htmx:abort'))"
        >

这里用了一个取巧的办法:Esc键触发时手动派发htmx:abort事件来取消当前请求。实际上更好的做法是让后端在收到Esc对应的行为时不做任何数据变更,返回原始的展示片段。HTMX社区里也有专门的扩展来处理这种”可取消的编辑”场景,但核心思路就是这样。

4.4 分页组件的HTMX实现

分页按钮的模板pagination.html

<!-- templates/pagination.html -->
        <div id="pagination" hx-swap-oob="true">
        {% if page > 1 %}
        <button
        hx-get="/users?page={{ page - 1 }}{% if query %}&q={{ query }}{% endif %}"
        hx-target="#table-body"
        hx-swap="innerHTML">
        上一页
    </button>
    {% endif %}

    {% for p in range(1, total_pages + 1) %}
    <button
    hx-get="/users?page={{ p }}{% if query %}&q={{ query }}{% endif %}"
    hx-target="#table-body"
    hx-swap="innerHTML"
    class="{{ 'active' if p == page else '' }}">
    {{ p }}
</button>
{% endfor %}

{% if page < total_pages %}
<button
hx-get="/users?page={{ page + 1 }}{% if query %}&q={{ query }}{% endif %}"
hx-target="#table-body"
hx-swap="innerHTML">
下一页
</button>
{% endif %}

<span class="page-info">第 {{ page }} / {{ total_pages }} 页</span>
</div>

分页组件通过hx-swap-oob="true"在每次表格数据更新时同步刷新自己。每个分页按钮都指向#table-body作为目标,点击后只替换表格内容区域,页面其他部分保持不变。URL里携带了当前的搜索关键词q,确保翻页时不会丢失搜索条件。

这里有一个容易踩的坑:如果你在分页按钮上忘了加查询参数q,用户搜索后翻页会丢失过滤条件,返回全部数据。HTMX本身不会自动维护这种上下文状态,需要你在生成每个按钮的URL时手动拼接。这个”状态传递”的问题在纯HTMX方案中确实需要多留意,后文会讨论一些应对策略。

4.5 搜索功能与空状态处理

搜索框在前面已经配置好了,这里补充一下搜索路由的处理逻辑:

# 搜索路由
@app.get("/users/search")
def search_users(q: str = ""):
if not q.strip():
# 空搜索词返回第一页全部数据
return redirect("/users?page=1")

users = db.search_users(name_like=q, page=1, per_page=10)
total_pages = db.count_search_pages(q, per_page=10)

table_html = render_template("table_rows.html", users=users)
pagination_html = render_template("pagination.html",
page=1,
total_pages=total_pages,
query=q)

return table_html + pagination_html

当搜索结果为空时,table_rows.html里那段{% if users|length == 0 %}的逻辑会渲染一个空状态提示行。这个空状态本身也是一个完整的<tr>,HTMX把它插入到#table-body中,替换掉之前的内容。用户看到的就是一条友好的提示信息,而不是一片空白。

空状态设计在实际项目中经常被忽略,但它直接影响用户对系统可靠性的感知。有了HTMX之后,空状态的切换变得特别自然——后端根据查询结果返回对应的HTML片段,前端不需要做任何条件判断。

五、踩坑记录与避坑指南

在实际使用HTMX的过程中,有几个坑是我反复踩过的,记录在这里希望能帮你节省一些调试时间。

5.1 浏览器历史记录的管理

默认情况下,HTMX不会把每次请求推入浏览器历史栈。这意味着用户点击了几个分页按钮后按浏览器的”后退”,页面并不会回到上一页。要解决这个问题,需要在触发请求的元素上添加hx-push-url属性:

<button
        hx-get="/users?page=2"
        hx-target="#table-body"
        hx-push-url="true">
        第2页
    </button>

设置hx-push-url="true"后,HTMX会把当前请求的URL推入历史栈。用户点击”后退”时,HTMX会拦截popstate事件,自动向之前的URL重新发起请求并更新页面。这个行为跟SPA框架的路由非常相似,但完全不需要额外的路由库。

如果希望URL更干净,可以传一个具体的路径字符串:hx-push-url="/users/page/2"。这样地址栏显示的URL会更友好,对SEO也有帮助。

5.2 CSRF保护怎么处理

HTMX本身不处理CSRF Token,但它的请求会携带标准的浏览器Cookie。如果你的后端框架使用Double Submit Cookie或Header Token的方式来防御CSRF,需要在HTMX的全局配置中设置请求头。可以在页面加载时通过JavaScript配置一次(是的,这里需要写一点点JS,但只此一次):

<script>
    document.body.addEventListener('htmx:configRequest', function(event) {
    // 从meta标签或cookie中读取CSRF Token
    var token = document.querySelector('meta[name="csrf-token"]').content;
    event.detail.headers['X-CSRF-Token'] = token;
    });
</script>

这个事件监听器会在每次HTMX请求发出前触发,给你机会修改请求头。虽然引入了几行JavaScript,但它属于全局基础设施,不是业务逻辑,维护成本很低。

5.3 表单提交后清空输入框

在新增用户的场景中,用户填写表单点击提交,HTMX用POST请求把数据发给后端,后端保存成功后返回新渲染的表格行。但表单本身还在页面上,输入框里残留着上一次填写的内容。HTMX提供了hx-reset属性来处理这个场景:

<form
        hx-post="/users"
        hx-target="#table-body"
        hx-swap="afterbegin"
        hx-reset="true">
        <input type="text" name="name" required>
        <input type="email" name="email" required>
        <button type="submit">添加用户</button>
    </form>

hx-reset="true"会在请求成功后将表单重置为初始状态。如果请求失败(HTTP 4xx或5xx),表单内容会保留,方便用户修改后重新提交。

5.4 处理服务器返回的错误

当后端校验失败时,应该返回HTTP 422(Unprocessable Entity)状态码,并在响应体中包含错误信息。HTMX检测到非2xx状态码后,默认不会替换目标元素的内容。这个行为可以通过hx-target-error来定制:

<form
    hx-post="/users"
    hx-target="#table-body"
    hx-target-error="#error-panel"
    hx-swap="innerHTML">
    <!-- 表单字段 -->
</form>
<div id="error-panel"></div>

这样成功响应会更新表格,错误响应会更新错误面板,两种路径互不干扰。后端返回的错误HTML片段可以包含具体的字段校验提示,用户体验比浏览器的默认弹窗好得多。

六、HTMX是否适合你的项目

经过这个后台管理模块的实践,我对HTMX的适用场景有了比较清晰的认识。它并不是银弹,有些场景下用HTMX反而会增加复杂度。

适合使用HTMX的情况:

  • 后端已经有一套成熟的模板渲染体系,不想引入前端构建流程。
  • 页面的交互模式以”请求-响应”为主,不需要复杂的前端状态管理。
  • 团队成员对后端模板语言(Jinja2、ERB、Blade等)比较熟悉,JavaScript经验相对薄弱。
  • 项目需要渐进式地增强交互体验,而不是推倒重来。
  • 对前端包体积有严格要求(HTMX压缩后只有14KB,零依赖)。

不太适合的情况:

  • 需要复杂客户端状态管理的应用(比如在线文档编辑器、可视化搭建工具)。
  • 大量使用WebSocket进行双向实时通信的场景(HTMX有扩展支持但不如专门的方案灵活)。
  • 需要离线能力或PWA支持的应用。
  • 团队已经深度使用React/Vue等框架且迁移成本过高。

一个务实的策略是混合使用。在同一个项目中,大部分CRUD类页面用HTMX处理,少数需要复杂交互的页面继续使用Vue或React组件。HTMX通过hx-swap-oob和自定义事件机制可以很好地与其他前端代码共存。我在实际项目中就这么干的——列表页、表单页用HTMX,数据可视化仪表盘用Vue组件,两者通过浏览器的CustomEvent通信,互不干扰。

七、总结

回过头来看,HTMX给我的最大启发不是技术层面的,而是思路层面的。它让我重新审视了”前端交互一定要用JSON API + 前端框架”这个默认假设。在Web开发的历史中,服务器端渲染HTML本来就是最原始也最健壮的模式,HTMX做的事情本质上是在这个模式上添加了一层声明式的局部更新能力,让它适应现代用户对交互体验的期待。

如果你正在维护一个传统的服务端渲染项目,觉得jQuery那一套事件绑定的写法越来越难维护,但又没有资源做彻底的前后端分离,HTMX值得你花一个下午试一试。从最不关键的一个列表页开始改起,加几个hx-属性上去,看看效果。你可能会像我一样,在某个瞬间突然觉得——原来事情可以这么简单。

完整的示例代码可以在HTMX官方文档的Examples章节找到更多参考,官方还提供了一个在线Playground,不用搭建本地环境就能快速验证想法。如果你在实际使用中遇到了本文没覆盖到的场景,HTMX的Discord社区响应速度很快,通常几个小时内就能得到解答。

HTMX实战笔记:用HTML属性构建现代Web交互,告别JavaScript疲劳
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html HTMX实战笔记:用HTML属性构建现代Web交互,告别JavaScript疲劳 https://www.taomawang.com/web/html/2352.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务