Rust Web框架Actix与HTMX集成库actix-htmx的使用,实现高效前端交互与动态页面渲染

Rust Web框架Actix与HTMX集成库actix-htmx的使用,实现高效前端交互与动态页面渲染

actix-htmx 是一个全面的中间件,用于结合 htmx 和 Actix Web 构建动态Web应用程序。

crates.io Apache 2.0 or MIT licensed Documentation

actix-htmx 提供了完整的解决方案,用于通过htmx和Actix Web构建动态Web应用程序。它提供了对htmx请求头的类型安全访问、简单的响应操作以及强大的事件触发能力。

特性

  • 请求检测:自动检测htmx请求、增强请求和历史恢复请求
  • 头部访问:类型安全访问所有htmx请求头(当前URL、目标、触发器、提示等)
  • 事件触发:在不同生命周期阶段触发自定义JavaScript事件(可选数据)
  • 响应控制:通过响应头完全控制htmx行为(重定向、刷新、交换、重定向等)
  • 类型安全:完全类型化的API,利用Rust的类型系统确保正确性
  • 零配置:开箱即用,具有合理的默认值
  • 性能:高效的头处理,开销最小

安装

将以下内容添加到你的 Cargo.toml

[dependencies]
actix-htmx = "0.3"
actix-web = "4"

快速开始

  1. 注册中间件 到你的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
}
  1. 在处理器中使用 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()
    }
}

这个示例展示了:

  1. 设置中间件
  2. 处理htmx和常规请求
  3. 使用响应头实现动态行为
  4. 事件触发

要运行这个示例,确保在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()
}

这个示例演示了:

  1. 使用actix-web和actix-htmx构建完整的CRUD应用
  2. HTMX的无缝集成
  3. 动态内容更新
  4. 事件触发机制
  5. 前后端交互的最佳实践

要运行这个应用,只需将代码保存为main.rs,然后执行:

cargo run

应用将在 http://localhost:8080 上运行。

回到顶部