<

1.HTMLX #

htmx 让你可以直接在 HTML 中使用 AJAXCSS 过渡WebSockets服务器推送事件,通过 属性 构建 现代用户界面,结合 简单性超文本的强大功能

htmx 体积小(~14k min.gz'd),无依赖可扩展,并且与 react 相比,减少 了 67% 的代码量。

1.1 使用HTMLX的优势 #

1.2 典型的应用场景 #

HTMLX特别适合那些希望保持页面轻量且可维护性高的项目。对于希望快速开发出交互式和动态网页的开发者来说,它是一个非常有用的工具。

2.项目准备 #

2.1 安装 #

npm install express

2.2 package.json #

package.json

{
  "scripts": {
    "start:dev": "nodemon app.js"
  },
}

2.3 app.js #

app.js

const express = require('express');
const path = require("path");
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/', (req, res) => {
    const directoryPath = path.join(__dirname, 'public');
    fs.readdir(directoryPath, (err, files) => {
        const fileLinks = files.map(file => {
            return `<li><a href="${file}">${file}</a></li>`;
        }).join('');
        res.send(`<ul>${fileLinks} </ul>`);
    });
});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

2.4 index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    hello
</body>
</html>

3.发送GET请求 #

3.1. index.html #

public/1.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button type="button" hx-get="/">
        发送GET请求
    </button>
</body>

</html>

3.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
+app.get('/', (req, res) => {
+   res.send('GET请求响应');
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

4.发送GET请求 #

4.1. index.html #

public/1.get.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <button type="button" hx-get="/get">
        发送GET请求
    </button>
</body>

</html>

4.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
+app.get('/get', (req, res) => {
+    res.send('GET请求响应');
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

5.发送POST请求 #

5.1. index.html #

public/2.post.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button type="button" hx-post="/post">
        发送POST请求
    </button>
</body>

</html>

5.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
+app.post('/post', (req, res) => {
+   res.send('POST请求响应');
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

6.触发器修饰符 #

触发器是 htmx 中非常强大的功能,它定义了在特定事件发生时,何时发送请求。触发器可以根据用户的行为、时间间隔、页面加载等事件来触发请求,提供了灵活的交互方式。

6.1. index.html #

public/3.trigger.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button type="button" hx-get="/time" hx-trigger="mouseenter once">
        mouseenter
    </button>
    <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>
</body>

</html>

6.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
+app.get('/time', (req, res) => {
+   res.send(new Date().toLocaleString());
+});
+app.get('/trigger_delay', (req, res) => {
+   res.send(req.query.q);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

7.触发器过滤器 #

7.1. index.html #

public/4.filter.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <div hx-get="/time" hx-trigger="click[1==1]">
        Control Click Me
    </div>
    <div hx-get="/time" hx-trigger="click[1==2]">
        Control Click Me
    </div>
    <div hx-get="/time" hx-trigger="click[ctrlKey]">
        Control Click Me
    </div>
</body>

</html>

8.特殊事件 #

8.1. index.html #

public/5.special.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <style>
        #revealed {
            height: 400px;
            background-color: green;
            margin-top: 150vh;
        }
        #intersect {
            height: 400px;
            background-color: green;
            margin-top: 200vh;
        }
    </style>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <div hx-get="/time" hx-trigger="load">
        load
    </div>
    <div hx-get="/time" hx-trigger="revealed" id="revealed">
    </div>
    <div hx-get="/time" hx-trigger="intersect threshold:.5" id="intersect">
    </div>
</body>
</html>

9.轮询 #

9.1. index.html #

public/6.pool.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <div hx-get="/time" hx-trigger="every 1s"></div>
    <div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML"></div>
</body>
</html>

9.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(new Date().toLocaleString());
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
+let timer = 1;
+app.get('/load_polling', (req, res) => {
+   if (timer++ < 6) {
+       res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
+   }
+   else { 
+       res.send('加载完成');
+   }
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

10.请求指示器 #

10.1. index.html #

public/7.indicator.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button hx-get="/delay">
        delay1
        <p class="htmx-indicator">loading.......</p>
    </button>
    <button hx-get="/delay" hx-indicator="#my-indicator">
        delay2
    </button>
    <p class="htmx-indicator" id="my-indicator">loading.......</p>
</body>

</html>

10.2. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(new Date().toLocaleString());
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
+   else {
        res.send('加载完成');
    }
});
+app.get('/delay', (req, res) => {
+   setTimeout(() => {
+       res.send('延迟响应');
+   }, 3000);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

11.发送数据 #

11.1. send.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <form>
        <input type="text" name="name" placeholder="Name">
        <input type="email" name="email" placeholder="Email">
        <button hx-post="/payload" hx-trigger="click" hx-target="#result">Send</button>
    </form>
    <div id="result"></div>
</body> 
</html>

11.2 include.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
    <script>
        function getEmail() {
            return document.querySelector('input[type="email"]').value;
        }
    </script>
</head>
<body>
    <input type="email" />
    <button hx-post="/payload" hx-target="#result" hx-vals='{"name":"name","email":"email"}'>发送json数据</button>
    <button hx-post="/payload" hx-target="#result" hx-vals='js:{"name":"name","email":getEmail()}'  hx-confirm="确认要发送吗?">发送js数据</button>
    <div id="result"></div>
</body>
</html>

11.3. sendjson.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
    <script>
        function getEmail() {
            return document.querySelector('input[type="email"]').value;
        }
    </script>
</head>
<body>
    <input type="email" id="my-email" name="email" />
    <input type="text" id="my-name" name="name" />
    <button hx-post="/payload" hx-target="#result" hx-vals='{"name":"name","email":"email"}'>发送json数据</button>
    <button hx-post="/payload" hx-target="#result" hx-vals='js:{"name":"name","email":getEmail()}'>发送js数据</button>
    <button hx-post="/payload" hx-target="#result" hx-include="#my-name,#my-email"  hx-confirm="确认要发送吗?">发送include数据</button>
    <div id="result"></div>
</body>
</html>

11.4. app.js #

app.js

const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(new Date().toLocaleString());
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
+app.post('/payload', (req, res) => {
+   const user = req.body;
+   user.id = Date.now();
+   res.send(`
+   <p>用户信息:</p>
+   <p>用户名:${user.name}</p>
+   <p>邮箱:${user.email}</p>
+   `);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

12.上传文件 #

12.1. index.html #

public/11.upload.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <form hx-post="/upload" hx-encoding="multipart/form-data">
        <input type="file" name="file">
        <button>上传</button>
    </form>
   <ul hx-get="/files" hx-trigger="load"></ul>
</body>
</html>

12.2. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
+const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+app.use(express.static('public'));
+app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(new Date().toLocaleString());
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
+app.get('/files', (req, res) => {
+   fs.readdir('./uploads', (err, files) => {
+       console.log(files);
+       if (err) {
+           res.send('读取文件失败');
+       } else {
+           res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
+       }
+   });
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

13.序列化请求 #

13.1. validate.html #

public/13.validate.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <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>
</body>

</html>

13.2. sync.html #

public/12.sync.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button id="firstRequest" hx-get="/firstRequest" hx-target="#result1">
        First Request
    </button>
    <button id="secondRequest" hx-get="/secondRequest" hx-target="#result2" hx-sync="#firstRequest:drop">
        Second Request drop
    </button>
    <button id="secondRequest" hx-get="/secondRequest" hx-target="#result2" hx-sync="#firstRequest:replace">
        Second Request replace
    </button>
    <button id="secondRequest" hx-get="/secondRequest" hx-target="#result2" hx-sync="#firstRequest:queue">
        Second Request queue
    </button>
    <div id="result1">
    </div>
    <div id="result2">
    </div>
</body>

</html>

13.3. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(new Date().toLocaleString());
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
+app.get('/firstRequest', (req, res) => {
+   setTimeout(() => {
+       res.send('firstRequestResponse');
+   }, 6000);
+});
+app.get('/secondRequest', (req, res) => {
+   setTimeout(() => {
+       res.send('secondRequestResponse');
+   }, 3000);
+});
+app.post('/validate', (req, res) => {
+   setTimeout(() => {
+       res.send('验证通过');
+   }, 6000);
+});
+
+app.post('/store', (req, res) => {
+   setTimeout(() => {
+       res.send('成功保存');
+   }, 3000);
+});
+
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

14.取消请求 #

14.1. index.html #

public/14.cancel.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <button type="button" id="requestButton" hx-get="/delay">
        发送请求
    </button>
    <button type="button" onclick="htmx.trigger('#requestButton', 'htmx:abort')">
        取消请求
    </button>
</body>
</html>

15.替换 #

交换类型(Swap)是 htmx 中用于控制服务器响应内容如何插入到 DOM 中的机制。通过指定不同的交换类型,你可以精确控制服务器返回的数据如何与现有的页面元素进行融合、替换、或删除。

htmx 提供了几种将返回的 HTML 替换到 DOM 中的方式。默认情况下,内容会替换目标元素的 innerHTML。你可以使用 swapping 属性,并设置以下任意值来修改这种行为:

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

15.1. index.html #

public/16.target.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
    <style>
        #targetParent {
            border: 5px solid red;
            padding: 10px;
        }

        #target {
            border: 5px solid blue;
            padding: 10px;
        }

        #targetChild1 {
            border: 5px solid green;
            padding: 10px;
        }

        #targetChild2 {
            border: 5px solid green;
            padding: 10px;
        }
    </style>
</head>

<body>
    <button hx-get="/time" hx-target="#target" hx-swap="beforebegin">beforebegin</button>
    <button hx-get="/time" hx-target="#target" hx-swap="afterbegin">afterbegin</button>
    <button hx-get="/time" hx-target="#target" hx-swap="beforeend">beforeend</button>
    <button hx-get="/time" hx-target="#target" hx-swap="afterend">afterend</button>
    <button hx-get="/time" hx-target="#target" hx-swap="delete">delete</button>
    <button hx-get="/time" hx-target="#target" hx-swap="none">none</button>
    <div id="targetParent">
        <!-- beforeBegin -->
        <div id="target">
            <!-- afterBegin -->
            <div id="targetChild1">targetChild1</div>
            <div id="targetChild2">targetChild2</div>
            <!-- beforeEnd -->
        </div>
        <!-- afterEnd -->
    </div>
</body>

</html>

15.3. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
+app.get('/', (req, res) => {
+   const directoryPath = path.join(__dirname, 'public');
+   fs.readdir(directoryPath, (err, files) => {
+       const fileLinks = files.map(file => {
+           return `<li><a href="${file}">${file}</a></li>`;
+       }).join('');
+       res.send(`<ul>${fileLinks} </ul>`);
+   });
+});
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
+   res.send(`<span>${new Date().toLocaleString()}</span>`);
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
app.get('/firstRequest', (req, res) => {
    setTimeout(() => {
        res.send('firstRequestResponse');
    }, 6000);
});
app.get('/secondRequest', (req, res) => {
    setTimeout(() => {
        res.send('secondRequestResponse');
    }, 3000);
});
app.post('/validate', (req, res) => {
    setTimeout(() => {
        res.send('验证通过');
    }, 6000);
});

app.post('/store', (req, res) => {
    setTimeout(() => {
        res.send('成功保存');
    }, 3000);
});

const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

16.越界替换 #

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

hx-swap-oob 是 htmx 提供的一个特殊功能,用于实现“带外”内容交换。这意味着当服务器返回内容时,该内容可以被插入到页面上与触发请求的元素无关的任何位置,而不是按照常规的方式替换或插入到触发请求的元素或指定的目标元素中。

hx-swap-oob 允许服务器响应包含多个独立片段,这些片段可以根据其自身的 hx-swap-oob 属性直接插入到页面的其他部分。这种机制特别适合复杂页面的部分更新操作,在单次请求中实现多个区域的同时更新。

16.1. index.html #

public/17.oob.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>

</head>

<body>
    <button hx-get="/oob" hx-target="#mainTarget">
        加载内容
    </button>
    <div id="mainTarget">
        主要目标
    </div>
    <div id="otherTarget">
    </div>
</body>

</html>

16.2. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/', (req, res) => {
    const directoryPath = path.join(__dirname, 'public');
    fs.readdir(directoryPath, (err, files) => {
        const fileLinks = files.map(file => {
            return `<li><a href="${file}">${file}</a></li>`;
        }).join('');
        res.send(`<ul>${fileLinks} </ul>`);
    });
});
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(`<span>${new Date().toLocaleString()}</span>`);
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
app.get('/firstRequest', (req, res) => {
    setTimeout(() => {
        res.send('firstRequestResponse');
    }, 6000);
});
app.get('/secondRequest', (req, res) => {
    setTimeout(() => {
        res.send('secondRequestResponse');
    }, 3000);
});
app.post('/validate', (req, res) => {
    setTimeout(() => {
        res.send('验证通过');
    }, 6000);
});

app.post('/store', (req, res) => {
    setTimeout(() => {
        res.send('成功保存');
    }, 3000);
});
+app.get('/oob', (req, res) => {
+   res.send(`
+       <span>主要的内容</span>
+       <div id="otherTarget" hx-swap-oob="true">其它内容</div>
+`);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

17.选择替换内容 #

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

17.1. index.html #

public/18.select.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>

</head>

<body>
    <button hx-get="/select" hx-target="#mainTarget" hx-select="#otherTarget">
        加载内容
    </button>
    <div id="mainTarget">
        主要目标
    </div>
</body>

</html>

17.2. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/', (req, res) => {
    const directoryPath = path.join(__dirname, 'public');
    fs.readdir(directoryPath, (err, files) => {
        const fileLinks = files.map(file => {
            return `<li><a href="${file}">${file}</a></li>`;
        }).join('');
        res.send(`<ul>${fileLinks} </ul>`);
    });
});
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(`<span>${new Date().toLocaleString()}</span>`);
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
app.get('/firstRequest', (req, res) => {
    setTimeout(() => {
        res.send('firstRequestResponse');
    }, 6000);
});
app.get('/secondRequest', (req, res) => {
    setTimeout(() => {
        res.send('secondRequestResponse');
    }, 3000);
});
app.post('/validate', (req, res) => {
    setTimeout(() => {
        res.send('验证通过');
    }, 6000);
});

app.post('/store', (req, res) => {
    setTimeout(() => {
        res.send('成功保存');
    }, 3000);
});
app.get('/oob', (req, res) => {
    res.send(`
+       主要的内容
        <div id="otherTarget" hx-swap-oob="true">其它内容</div>
`);
});
+app.get('/select', (req, res) => {
+   res.send(`
+       <span>主要的内容</span>
+       <div id="otherTarget">其它内容</div>
+`);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

18.动画 #

htmx 允许您仅使用 HTML 和 CSS 在许多情况下使用 CSS 过渡。

18.1. index.html #

public/19.animation.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
    <style>
        .circle {
            border-radius: 50%;
            background-color: red;
            transition: all 1s ease-in;
        }
    </style>
</head>

<body>
    <div id="circle" class="circle" hx-get="/bigger" hx-swap="outerHTML" style="width:100px;height:100px;">

    </div>
</body>

</html>

18.2. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/', (req, res) => {
    const directoryPath = path.join(__dirname, 'public');
    fs.readdir(directoryPath, (err, files) => {
        const fileLinks = files.map(file => {
            return `<li><a href="${file}">${file}</a></li>`;
        }).join('');
        res.send(`<ul>${fileLinks} </ul>`);
    });
});
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(`<span>${new Date().toLocaleString()}</span>`);
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
app.get('/firstRequest', (req, res) => {
    setTimeout(() => {
        res.send('firstRequestResponse');
    }, 6000);
});
app.get('/secondRequest', (req, res) => {
    setTimeout(() => {
        res.send('secondRequestResponse');
    }, 3000);
});
app.post('/validate', (req, res) => {
    setTimeout(() => {
        res.send('验证通过');
    }, 6000);
});

app.post('/store', (req, res) => {
    setTimeout(() => {
        res.send('成功保存');
    }, 3000);
});
app.get('/oob', (req, res) => {
    res.send(`
        主要的内容
        <div id="otherTarget" hx-swap-oob="true">其它内容</div>
`);
});
app.get('/select', (req, res) => {
    res.send(`
        <span>主要的内容</span>
        <div id="otherTarget">其它内容</div>
`);
});
+app.get('/bigger', (req, res) => {
+   res.send(`
+     <div id="circle" class="circle" hx-get="http://localhost:1330/bigger" hx-swap="outerHTML" style="width:200px;height:200px;">
+
+   </div>
+`);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

19.htmx:load #

19.1. index.html #

public/20.load.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>

</head>

<body>
    <script>
        document.body.addEventListener('htmx:load', function (event) {
            console.log('htmx:load', event);
        });
        htmx.on("htmx:load", function (event) {
            console.log('htmx.on("htmx:load")', event);
        });
        htmx.onLoad((target) => {
            console.log('htmx.onLoad', event);
        })
    </script>
</body>

</html>

20.使用事件修改交换行为 #

modifying_swapping_behavior_with_events

20.1. index.html #

public/21.swap.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>
<body>
    <script>
        htmx.on("htmx:beforeSwap", (evt) => {
            console.log("开始Swap");
            console.log(evt.detail.elt);
        })
        htmx.on("htmx:afterSwap", (evt) => {
            console.log("Swap完成");
        })
    </script>
    <button hx-get="/time" hx-target="#target">
        获取时间
    </button>
    <div id="target"></div>
</body>
</html>

21.请求事件 #

21.1. index.html #

public/22.请求事件.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <script>
        htmx.on("htmx:beforeRequest", (evt) => {
            console.log("htmx:beforeRequest", evt.detail.requestConfig.headers);
        })
        htmx.on("htmx:afterRequest", (evt) => {
            console.log("htmx:afterRequest", evt.detail.xhr.response);
        })
    </script>
    <button hx-get="/time" hx-target="#target">
        获取时间
    </button>
    <div id="target"></div>
</body>

</html>

22.请求失败 #

22.1. index.html #

public/23.请求失败.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <script>
        htmx.on("htmx:responseError", (evt) => {
            console.log(evt);
            alert("请求失败");
        });
    </script>
    <button hx-get="/times" hx-target="#target">
        获取时间
    </button>
    <div id="target"></div>
</body>

</html>

23.请求拦截器 #

23.1. index.html #

public/24.请求拦截器.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX</title>
    <script src="/htmx.min.js"></script>
</head>

<body>
    <button hx-get="/params" hx-target="#target">
        params
    </button>
    <div id="target"></div>
    <script>
        htmx.on("htmx:configRequest", (evt) => {
            evt.detail.headers["Authorization"] = "Bearer token";
            evt.detail.parameters["version"] = "1.0";
        })
        htmx.logger = function (elt, event, data) {
             console.log(event, data, elt);
        }
    </script>
</body>

</html>

23.2. app.js #

app.js

const express = require('express');
const app = express();
const multer = require("multer");
const path = require("path");
const fs = require('fs');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use(express.static('uploads'));
const storage = multer.diskStorage({
    destination: (_req, _file, cb) => {
        cb(null, './uploads/')
    },
    filename: (req, file, cb) => {
        cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
    }
})
const upload = multer({ storage: storage });
app.get('/', (req, res) => {
    const directoryPath = path.join(__dirname, 'public');
    fs.readdir(directoryPath, (err, files) => {
        const fileLinks = files.map(file => {
            return `<li><a href="${file}">${file}</a></li>`;
        }).join('');
        res.send(`<ul>${fileLinks} </ul>`);
    });
});
app.get('/get', (req, res) => {
    res.send('GET请求响应');
});
app.post('/post', (req, res) => {
    res.send('POST请求响应');
});
app.get('/time', (req, res) => {
    res.send(`<span>${new Date().toLocaleString()}</span>`);
});
app.get('/trigger_delay', (req, res) => {
    res.send(req.query.q);
});
let timer = 1;
app.get('/load_polling', (req, res) => {
    if (timer++ < 6) {
        res.send(`<div hx-get="/load_polling" hx-trigger="load delay:1s" hx-swap="outerHTML">${".".repeat(timer)}</div>`);
    }
    else {
        res.send('加载完成');
    }
});
app.get('/delay', (req, res) => {
    setTimeout(() => {
        res.send('延迟响应');
    }, 3000);
});
app.post('/payload', (req, res) => {
    const user = req.body;
    user.id = Date.now();
    res.send(`
    <p>用户信息:</p>
    <p>用户名:${user.name}</p>
    <p>邮箱:${user.email}</p>
    `);
});
app.post("/upload", upload.single("file"), async (req, res) => {
    const filePath = req.file.path;
    console.log(filePath);
    res.send(`<b> 上传成功</b>: ${filePath}`);
})
app.get('/files', (req, res) => {
    fs.readdir('./uploads', (err, files) => {
        console.log(files);
        if (err) {
            res.send('读取文件失败');
        } else {
            res.send(files.map(file => (`<li><a href="/${file}">${file}</a></li>`)).join(''));
        }
    });
});
app.get('/firstRequest', (req, res) => {
    setTimeout(() => {
        res.send('firstRequestResponse');
    }, 6000);
});
app.get('/secondRequest', (req, res) => {
    setTimeout(() => {
        res.send('secondRequestResponse');
    }, 3000);
});
app.post('/validate', (req, res) => {
    setTimeout(() => {
        res.send('验证通过');
    }, 6000);
});

app.post('/store', (req, res) => {
    setTimeout(() => {
        res.send('成功保存');
    }, 3000);
});
app.get('/oob', (req, res) => {
    res.send(`
        主要的内容
        <div id="otherTarget" hx-swap-oob="true">其它内容</div>
`);
});
app.get('/select', (req, res) => {
    res.send(`
        <span>主要的内容</span>
        <div id="otherTarget">其它内容</div>
`);
});
app.get('/bigger', (req, res) => {
    res.send(`
      <div id="circle" class="circle" hx-get="http://localhost:1330/bigger" hx-swap="outerHTML" style="width:200px;height:200px;">

    </div>
`);
});

+app.get('/params', (req, res) => {
+   const authorization = req.headers['authorization'];
+   const version = req.query.version;
+   res.send(`authorization: ${authorization}, version: ${version}`);
+});
const PORT = process.env.PORT || 80;
app.listen(PORT, () => {
    console.log(`App is running on port ${PORT}`);
});

HTMX #

  1. htmx 请求类型

    • hx-get:用于发送 GET 请求。
    • hx-post:用于发送 POST 请求。
  2. 触发器 (Triggers)

    • hx-trigger="mouseenter once":在鼠标进入元素时触发,并且只触发一次。
    • hx-trigger="keyup changed delay:500ms":在键盘按键抬起或输入变化时触发,且有500毫秒的延迟。
    • hx-trigger="click[条件]":基于条件触发(如 click[1==1]click[ctrlKey])。
    • hx-trigger="load":在页面加载时触发。
    • hx-trigger="revealed":当元素在视口中被显示时触发。
    • hx-trigger="intersect threshold:.5":当元素与视口相交超过50%时触发。
    • hx-trigger="every 1s":每隔1秒触发一次请求。
    • hx-trigger="load delay:1s":页面加载后延迟1秒触发请求。
  3. 交换类型 (Swap)

    • hx-swap="outerHTML":替换整个元素的 HTML。
    • hx-swap="beforebegin":在目标元素前插入内容。
    • hx-swap="afterbegin":在目标元素内部的第一个子元素之前插入内容。
    • hx-swap="beforeend":在目标元素内部的最后一个子元素之后插入内容。
    • hx-swap="afterend":在目标元素后插入内容。
    • hx-swap="delete":删除目标元素。
    • hx-swap="none":不交换内容。
  4. 加载指示器 (Indicators)

    • htmx-indicator:用于显示加载状态的指示器。
    • hx-indicator:指定自定义的加载指示器元素。
  5. 请求配置 (Request Configuration)

    • hx-encoding="multipart/form-data":指定请求的编码类型(用于文件上传)。
    • hx-vals:用于指定发送的数据(可以是 JSON 字符串或 JavaScript 对象)。
    • hx-include:指定附加的元素数据一起发送。
    • hx-confirm:在发送请求前显示确认对话框。
    • hx-sync:控制请求的同步方式(dropreplacequeue)。
  6. 事件 (Events)

    • htmx:beforeRequest:请求发送前触发的事件。
    • htmx:afterRequest:请求发送后触发的事件。
    • htmx:beforeSwap:内容交换前触发的事件。
    • htmx:afterSwap:内容交换后触发的事件。
    • htmx:responseError:响应错误时触发的事件。
    • htmx:load:内容加载完成时触发的事件。
    • htmx:configRequest:配置请求时触发的事件(用于修改请求的 headers 和参数)。
    • htmx:abort:用于中止请求的事件。
  7. 其他功能

    • hx-target:指定交换内容的目标元素。
    • hx-select:从响应中选择要替换的内容。
    • hx-oob(Out of Band):从响应中替换非目标元素的内容。
    • htmx.onLoad:在内容加载后执行自定义逻辑。
    • htmx.trigger:手动触发 htmx 事件(如 htmx:abort 用于取消请求)。

1. htmx 请求类型 #

1.1 hx-get #

1.2 hx-post #

2. 触发器 (Triggers) #

触发器是 htmx 中非常强大的功能,它定义了在特定事件发生时,何时发送请求。触发器可以根据用户的行为、时间间隔、页面加载等事件来触发请求,提供了灵活的交互方式。

2.1 hx-trigger=mouseenter once #

2.2 hx-trigger=keyup changed delay:500ms #

2.3 hx-trigger=click[条件] #

2.4 hx-trigger=load #

2.5 hx-trigger=revealed #

2.6 hx-trigger=intersect threshold:.5 #

2.7 hx-trigger=every 1s #

2.8 hx-trigger=load delay:1s #

3. 交换类型 (Swap) #

交换类型(Swap)是 htmx 中用于控制服务器响应内容如何插入到 DOM 中的机制。通过指定不同的交换类型,你可以精确控制服务器返回的数据如何与现有的页面元素进行融合、替换、或删除。

3.1 hx-swap=outerHTML #

3.2 hx-swap=beforebegin #

3.3 hx-swap=afterbegin #

3.4 hx-swap=beforeend #

3.5 hx-swap=afterend #

3.6 hx-swap=delete #

3.7 hx-swap=none #

3.8 hx-swap-oob #

功能

hx-swap-oob 是 htmx 提供的一个特殊功能,用于实现“带外”内容交换。这意味着当服务器返回内容时,该内容可以被插入到页面上与触发请求的元素无关的任何位置,而不是按照常规的方式替换或插入到触发请求的元素或指定的目标元素中。

hx-swap-oob 允许服务器响应包含多个独立片段,这些片段可以根据其自身的 hx-swap-oob 属性直接插入到页面的其他部分。这种机制特别适合复杂页面的部分更新操作,在单次请求中实现多个区域的同时更新。

使用场景

工作原理

在服务器响应中,任何具有 hx-swap-oob 属性的元素都会被解析,并插入到页面上的指定位置,而不需要依赖于触发请求的元素。

示例

假设我们有以下页面结构:

<button hx-get="/update" hx-target="#mainContent">更新内容</button>
<div id="mainContent">
    主内容区域
</div>
<div id="sideBar">
    侧边栏内容
</div>

服务器返回的响应可能是这样:

<div hx-swap-oob="true" id="mainContent">
    更新后的主内容区域
</div>
<div hx-swap-oob="true" id="sideBar">
    更新后的侧边栏内容
</div>

在这个响应中:

使用示例

  1. HTML 代码:

    <button hx-get="/update" hx-target="#mainContent">更新内容</button>
    <div id="mainContent">
        主内容区域
    </div>
    <div id="sideBar">
        侧边栏内容
    </div>
    
  2. 服务器返回的响应:

    <div hx-swap-oob="true" id="mainContent">
        更新后的主内容区域
    </div>
    <div hx-swap-oob="true" id="sideBar">
        更新后的侧边栏内容
    </div>
    

当用户点击按钮时,页面的主内容区域和侧边栏内容都会同时更新,即使只有一个请求被发出。

总结

hx-swap-oob 是一种非常强大的技术,允许你在不直接依赖于请求触发元素或其指定目标的情况下,更新页面的多个部分。它简化了复杂的页面更新逻辑,特别是在服务器端渲染返回多个独立的内容片段时。通过 hx-swap-oob,你可以实现更复杂、更细粒度的页面更新操作,增强用户体验。

4. 加载指示器 (Indicators) #

加载指示器是 htmx 中用来表示正在进行网络请求的元素。它通常用于向用户展示请求的状态(例如“加载中...”),从而提升用户体验,特别是在请求需要较长时间处理的情况下。

4.1 htmx-indicator #

4.2 hx-indicator #

这些指示器有助于用户了解请求的处理状态,避免用户因没有视觉反馈而感到困惑或反复点击按钮。

5. 请求配置 (Request Configuration) #

请求配置涉及到如何定制和管理 htmx 发送的请求。通过配置不同的属性,你可以控制请求的数据格式、附加的数据、以及在请求前后的处理方式。

5.1 hx-encoding #

5.2 hx-vals #

5.3 hx-include #

5.4 hx-confirm #

5.5 hx-sync #

6. 事件 (Events) #

htmx 提供了丰富的事件系统,允许你在请求的不同阶段进行自定义处理。这些事件让你能够在请求发送前、响应接收后以及内容交换前后执行特定的逻辑,从而实现更精细的控制。

6.1 htmx:beforeRequest #

6.2 htmx:afterRequest #

6.3 htmx:beforeSwap #

6.4 htmx:afterSwap #

6.5 htmx:responseError #

6.6 htmx:load #

6.7 htmx:configRequest #

6.8 htmx:abort #

7. 其他功能 (Other Features) #

htmx 除了前面提到的核心功能,还提供了一些其他有用的特性,这些特性可以帮助你实现更复杂的交互和行为。

7.1 hx-target #

7.2 hx-select #

7.3 hx-oob (Out of Band) #

7.4 htmx.onLoad #

7.5 htmx.trigger #