Rust Web框架Actix与HTMX集成库actix-htmx的使用,实现高效前端交互与动态页面渲染
Rust Web框架Actix与HTMX集成库actix-htmx的使用,实现高效前端交互与动态页面渲染
actix-htmx
是一个全面的中间件,用于结合 htmx 和 Actix Web 构建动态Web应用程序。
actix-htmx
提供了完整的解决方案,用于通过htmx和Actix Web构建动态Web应用程序。它提供了对htmx请求头的类型安全访问、简单的响应操作以及强大的事件触发能力。
特性
- 请求检测:自动检测htmx请求、增强请求和历史恢复请求
- 头部访问:类型安全访问所有htmx请求头(当前URL、目标、触发器、提示等)
- 事件触发:在不同生命周期阶段触发自定义JavaScript事件(可选数据)
- 响应控制:通过响应头完全控制htmx行为(重定向、刷新、交换、重定向等)
- 类型安全:完全类型化的API,利用Rust的类型系统确保正确性
- 零配置:开箱即用,具有合理的默认值
- 性能:高效的头处理,开销最小
安装
将以下内容添加到你的 Cargo.toml
:
[dependencies]
actix-htmx = "0.3"
actix-web = "4"
快速开始
- 注册中间件 到你的Actix Web应用:
use actix_htmx::HtmxMiddleware;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(HtmxMiddleware) // 添加这一行
.route("/", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
- 在处理器中使用
Htmx
提取器:
use actix_htmx::Htmx;
use actix_web::{HttpResponse, Responder};
async fn index(htmx: Htmx) -> impl Responder {
if htmx.is_htmx {
// 这是一个htmx请求 - 返回部分HTML
HttpResponse::Ok().body("<div>Partial content for htmx</div>")
} else {
// 常规请求 - 返回完整页面
HttpResponse::Ok().body(r##"
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/htmx.org@2.0.6"></script>
</head>
<body>
<div id="content">
<button hx-get="/" hx-target="#content">
Click me for htmx!
</button>
</div>
</body>
</html>
"##)
}
}
使用示例
访问请求信息
use actix_htmx::Htmx;
use actix_web::{HttpResponse, Responder};
async fn handler(htmx: Htmx) -> impl Responder {
// 检查是否是htmx请求
if htmx.is_htmx {
println!("This is an htmx request!");
// 访问htmx特定信息
if let Some(target) = htmx.target() {
println!("Target element: {}", target);
}
if let Some(trigger) = htmx.trigger() {
println!("Triggered by element: {}", trigger);
}
if let Some(current_url) = htmx.current_url() {
println!("Current page URL: {}", current_url);
}
}
// 检查增强请求
if htmx.boosted {
println!("This is a boosted request!");
}
// 检查历史恢复
if htmx.history_restore_request {
println!("This is a history restore request!");
}
HttpResponse::Ok().body("Hello, htmx!")
}
控制响应行为
use actix_htmx::{Htmx, SwapType, TriggerType};
use actix_web::{HttpResponse, Responder};
async fn create_item(htmx: Htmx) -> impl Responder {
// 触发自定义JavaScript事件
htmx.trigger_event(
"itemCreated".to_string(),
Some(r#"{"id": 123, "name": "New Item"}"#.to_string()),
Some(TriggerType::Standard)
);
// 更改内容交换方式
htmx.reswap(SwapType::OuterHtml);
// 更新URL而不导航
htmx.push_url("/items/123".to_string());
// 创建成功后重定向
htmx.redirect("/items".to_string());
HttpResponse::Ok().body("<div>Item created!</div>")
}
事件触发
htmx支持在不同生命周期阶段触发自定义事件:
use actix_htmx::{Htmx, TriggerType};
use actix_web::{HttpResponse, Responder};
async fn handler(htmx: Htmx) -> impl Responder {
// 收到响应后立即触发
htmx.trigger_event(
"dataLoaded".to_string(),
None,
Some(TriggerType::Standard)
);
// 内容交换到DOM后触发
htmx.trigger_event(
"contentSwapped".to_string(),
Some(r#"{"timestamp": "2024-01-01"}"#.to_string()),
Some(TriggerType::AfterSwap)
);
// htmx完成后触发(动画等)
htmx.trigger_event(
"pageReady".to_string(),
None,
Some(TriggerType::AfterSettle)
);
HttpResponse::Ok().body("<div>Content updated!</div>")
}
高级响应控制
use actix_htmx::{Htmx;
use actix_web::{HttpResponse, Responder};
async fn advanced_handler(htmx: Htmx) -> impl Responder {
// 更改此响应的目标元素
htmx.retarget("#different-element".to_string());
// 从响应中选择特定内容
htmx.reselect(".important-content".to_string());
// 替换浏览器历史中的URL(不创建新历史条目)
htmx.replace_url("/new-path".to_string());
// 刷新整个页面
htmx.refresh();
// 使用htmx重定向(无完整页面重载)
htmx.redirect_with_swap("/dashboard".to_string());
HttpResponse::Ok().body(r#"
<div class="important-content">
This content will be selected and swapped!
</div>
<div class="other-content">
This won't be swapped due to reselect.
</div>
"#)
}
交换类型
SwapType
枚举提供了所有htmx交换策略:
InnerHtml
- 替换内部HTML(默认)OuterHtml
- 替换整个元素BeforeBegin
- 在元素前插入AfterBegin
- 在元素开头插入BeforeEnd
- 在元素末尾插入AfterEnd
- 在元素后插入Delete
- 删除元素None
- 不交换内容
触发类型
事件可以在不同点触发:
Standard
- 收到响应后立即触发AfterSwap
- 内容交换到DOM后触发AfterSettle
- htmx完成后触发(动画等)
完整示例代码
以下是一个完整的示例,展示如何使用actix-htmx构建一个简单的待办事项应用:
use actix_htmx::{Htmx, HtmxMiddleware};
use actix_web::{
web, App, HttpResponse, HttpServer, Responder,
};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: usize,
title: String,
completed: bool,
}
struct AppState {
todos: Mutex::<Vec<Todo>>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
todos: Mutex::new(vec![
Todo {
id: 1,
title: "Learn Rust".to_string(),
completed: false,
},
Todo {
id: 2,
title: "Build a web app".to_string(),
completed: false,
},
]),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.wrap(HtmxMiddleware)
.route("/", web::get().to(index))
.route("/todos", web::get().to(get_todos))
.route("/todos/{id}/toggle", web::post().to(toggle_todo))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn index(htmx: Htmx) -> impl Responder {
if htmx.is_htmx {
HttpResponse::Ok().body("")
} else {
HttpResponse::Ok().body(format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<script src="https://unpkg.com/htmx.org@1.9.0"></script>
<style>
.completed {{ text-decoration: line-through; opacity: 0.7; }}
</style>
</head>
<body>
<h1>Todo App</h1>
<div id="todos" hx-get="/todos" hx-trigger="load"></div>
</body>
</html>
"#
))
}
}
async fn get_todos(data: web::Data<AppState>) -> impl Responder {
let todos = data.todos.lock().unwrap();
let mut html = String::new();
html.push_str("<ul>");
for todo in todos.iter() {
html.push_str(&format!(
r#"
<li class="{}" hx-post="/todos/{}/toggle" hx-target="#todos">
{}
</li>
"#,
if todo.completed { "completed" } else { "" },
todo.id,
todo.title
));
}
html.push_str("</ul>");
HttpResponse::Ok().body(html)
}
async fn toggle_todo(
data: web::Data<AppState>,
path: web::Path<usize>,
htmx: Htmx,
) -> impl Responder {
let id = path.into_inner();
let mut todos = data.todos.lock().unwrap();
if let Some(todo) = todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
if htmx.is_htmx {
get_todos(data).await
} else {
HttpResponse::Found()
.append_header(("Location", "/"))
.finish()
}
}
这个示例展示了:
- 设置中间件
- 处理htmx和常规请求
- 使用响应头实现动态行为
- 事件触发
要运行这个示例,确保在Cargo.toml中添加了所有必要的依赖项:
[dependencies]
actix-htmx = "0.3"
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
1 回复
Rust Web框架Actix与HTMX集成库actix-htmx的使用
介绍
actix-htmx
是一个将HTMX前端库与Actix-web框架集成的Rust库,它允许开发者构建现代化的Web应用,同时保持后端渲染的简洁性。HTMX通过扩展HTML属性,让开发者可以直接在标记中声明AJAX请求、CSS过渡效果等,而无需编写大量JavaScript代码。
主要特性
- 无缝集成Actix-web和HTMX
- 提供HTMX请求头的类型安全访问
- 简化HTMX响应头的设置
- 支持HTMX的所有核心功能
- 保持Rust的类型安全和性能优势
安装
在Cargo.toml中添加依赖:
[dependencies]
actix-web = "4"
actix-htmx = "0.4"
基本使用方法
1. 初始化Actix-web应用并集成HTMX
use actix_web::{App, HttpServer, web};
use actix_htmx::Htmx;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(web::resource("/").to(index))
.service(web::resource("/clicked").to(clicked))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
2. 基本HTMX交互示例
use actix_web::{HttpRequest, HttpResponse};
use actix_htmx::Htmx;
async fn index() -> HttpResponse {
HttpResponse::Ok().body(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Actix + HTMX</title>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
</head>
<body>
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
</body>
</html>
"#,
)
}
async fn clicked(htmx: Htmx) -> HttpResponse {
if htmx.is_boosted() {
HttpResponse::Ok().body("<div>Button was clicked with HTMX!</div>")
} else {
HttpResponse::Ok().body("<div>Fallback for non-HTMX requests</div>")
}
}
3. 使用HTMX请求头
use actix_web::{HttpRequest, HttpResponse};
use actix_htmx::Htmx;
async fn advanced_example(htmx: Htmx) -> HttpResponse {
if htmx.is_htmx() {
// 检查是否是HTMX请求
let current_url = htmx.current_url().unwrap_or_default();
let prompt = htmx.prompt().unwrap_or_default();
let target = htmx.target().unwrap_or_default();
HttpResponse::Ok()
.insert_header(("HX-Trigger", "myEvent"))
.body(format!(
"Current URL: {}, Prompt: {}, Target: {}",
current_url, prompt, target
))
} else {
HttpResponse::Ok().body("Regular HTTP request")
}
}
4. 动态内容加载
async fn load_content(htmx: Htmx) -> HttpResponse {
if htmx.is_htmx() {
let items = vec!["Item 1", "Item 2", "Item 3"];
let content = items.iter()
.map(|item| format!("<li>{}</li>", item))
.collect::<String>();
HttpResponse::Ok()
.insert_header(("HX-Reswap", "beforeend"))
.body(format!("<ul>{}</ul>", content))
} else {
HttpResponse::Ok().body("Full page content")
}
}
高级用法
1. 触发客户端事件
async fn trigger_event() -> HttpResponse {
HttpResponse::Ok()
.insert_header(("HX-Trigger", "showMessage"))
.insert_header(("HX-Trigger-After-Settle", r#"{"showNotification":{"title":"Success","message":"Action completed"}}"#))
.body("Action performed successfully")
}
2. 处理表单提交
use actix_web::{web, HttpResponse};
use serde::Deserialize;
#[derive(Deserialize)]
struct FormData {
name: String,
email: String,
}
async fn handle_form(form: web::Form极简主义Web应用开发:使用Rust和HTMX构建现代化应用
以下是一个完整的示例,展示如何使用actix-web和actix-htmx构建一个简单的待办事项应用:
```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_htmx::Htmx;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
// 待办事项数据结构
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: usize,
title: String,
completed: bool,
}
// 应用状态
struct AppState {
todos: Mutex<Vec<Todo>>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 初始化应用状态
let app_state = web::Data::new(AppState {
todos: Mutex::new(vec![
Todo {
id: 1,
title: "Learn Rust".to_string(),
completed: false,
},
Todo {
id: 2,
title: "Build a web app".to_string(),
completed: false,
},
]),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.service(web::resource("/").to(index))
.service(web::resource("/todos").to(get_todos))
.service(web::resource("/todos/{id}/toggle").to(toggle_todo))
.service(web::resource("/todos").route(web::post().to(add_todo)))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
// 首页
async fn index() -> HttpResponse {
HttpResponse::Ok().body(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Rust + HTMX Todo App</title>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<style>
.completed { text-decoration: line-through; color: #888; }
.todo-list { margin: 20px 0; }
.todo-item { padding: 8px; margin: 4px 0; }
</style>
</head>
<body>
<h1>Todo App</h1>
<form hx-post="/todos" hx-swap="none">
<input type="text" name="title" placeholder="New todo" required>
<button type="submit">Add</button>
</form>
<div hx-get="/todos" hx-trigger="load, todoChanged from:body">
Loading todos...
</div>
</body>
</html>
"#,
)
}
// 获取所有待办事项
async fn get_todos(data: web::Data<AppState>, htmx: Htmx) -> impl Responder {
let todos = data.todos.lock().unwrap();
let html = todos.iter().map(|todo| {
let completed_class = if todo.completed { "completed" } else { "" };
format!(
r#"<div class="todo-item {completed_class}">
<input type="checkbox"
hx-post="/todos/{}/toggle"
hx-trigger="click"
hx-target="closest .todo-item"
{} />
{}</div>"#,
todo.id,
if todo.completed { "checked" } else { "" },
todo.title
)
}).collect::<String>();
if htmx.is_htmx() {
HttpResponse::Ok()
.insert_header(("HX-Reswap", "innerHTML"))
.body(html)
} else {
HttpResponse::Ok().body(html)
}
}
// 切换待办事项完成状态
async fn toggle_todo(
path: web::Path<usize>,
data: web::Data<AppState>,
) -> impl Responder {
let id = path.into_inner();
let mut todos = data.todos.lock().unwrap();
if let Some(todo) = todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
HttpResponse::Ok()
.insert_header(("HX-Trigger", "todoChanged"))
.finish()
}
// 添加新待办事项
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
async fn add_todo(
form: web::Form<NewTodo>,
data: web::Data<AppState>,
) -> impl Responder {
let mut todos = data.todos.lock().unwrap();
let new_id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;
todos.push(Todo {
id: new_id,
title: form.title.clone(),
completed: false,
});
HttpResponse::Ok()
.insert_header(("HX-Trigger", "todoChanged"))
.finish()
}
这个示例演示了:
- 使用actix-web和actix-htmx构建完整的CRUD应用
- HTMX的无缝集成
- 动态内容更新
- 事件触发机制
- 前后端交互的最佳实践
要运行这个应用,只需将代码保存为main.rs,然后执行:
cargo run
应用将在 http://localhost:8080 上运行。