SpringBoot 与 HTMX:现代 Web 开发的高效组合

在当今的 Web 开发领域,前后端分离已成为主流趋势。

传统的全栈框架往往需要复杂的模板引擎来处理视图逻辑,而前端框架如 React、Vue 等虽然强大,但也带来了学习曲线陡峭、构建复杂等问题。

本文将介绍一种轻量级的解决方案 ------ 结合 Spring Boot 与 HTMX,实现高效、简洁的前后端分离开发。

为什么选择 SpringBoot 与 HTMX?

Spring Boot 是 Java 生态中最流行的应用开发框架之一,它提供了自动配置、嵌入式服务器等特性,让开发者可以快速搭建企业级应用。

而 HTMX 是一个轻量级的 JavaScript 库,它允许你使用 HTML 属性直接与服务器进行 AJAX 通信,无需编写大量的 JavaScript 代码。

这种组合既保留了传统 HTML 的简单性,又具备现代 Web 应用的交互性。

项目架构概述

我们将构建一个简单的任务管理应用,采用前后端完全分离的架构:

  • 后端:Spring Boot REST API
  • 前端:纯 HTML + HTMX + doT.js + Tailwind CSS

这种架构使得前后端可以独立开发、测试和部署,同时保持高效的通信和良好的用户体验。

后端实现

首先,让我们创建 SpringBoot 后端

java 复制代码
package com.example.taskmanager;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@SpringBootApplication
@RestController
@RequestMapping("/api/tasks")
public class TaskManagerApplication {

    // 内存中的任务存储
    private List<Task> tasks = new ArrayList<>();

    public static void main(String[] args) {
        SpringApplication.run(TaskManagerApplication.class, args);
    }

    // 任务模型
    record Task(String id, String title, boolean completed) {}

    // 获取所有任务
    @GetMapping
    public List<Task> getAllTasks() {
        return tasks;
    }

    // 创建新任务
    @PostMapping
    public Task createTask(@RequestBody Task task) {
        Task newTask = new Task(UUID.randomUUID().toString(), task.title(), false);
        tasks.add(newTask);
        return newTask;
    }

    // 更新任务状态
    @PutMapping("/{id}/toggle")
    public Task toggleTask(@PathVariable String id) {
        Task task = tasks.stream()
                .filter(t -> t.id().equals(id))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Task not found"));
        
        Task updatedTask = new Task(task.id(), task.title(), !task.completed());
        tasks.remove(task);
        tasks.add(updatedTask);
        return updatedTask;
    }

    // 删除任务
    @DeleteMapping("/{id}")
    public void deleteTask(@PathVariable String id) {
        tasks.removeIf(t -> t.id().equals(id));
    }
}

这个后端实现了基本的 CRUD 操作:获取任务列表、创建新任务、切换任务状态和删除任务。

为了方便演示和快速能够运行DEOM,所有数据都存储在内存中的 List 中。

前端实现

接下来是前端部分,我们将使用 HTMX 来处理与后端的交互

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/olado/doT@master/doT.min.js"></script>

    <!-- Tailwind 配置 -->
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        danger: '#EF4444',
                        dark: '#1F2937',
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>

    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .task-item {
                @apply flex items-center justify-between p-4 mb-3 rounded-lg transition-all duration-300;
            }
            .task-item-complete {
                @apply bg-gray-100 text-gray-500 line-through;
            }
            .btn {
                @apply px-4 py-2 rounded-lg font-medium transition-all duration-200;
            }
            .btn-primary {
                @apply bg-primary text-white hover:bg-primary/90;
            }
            .btn-danger {
                @apply bg-danger text-white hover:bg-danger/90;
            }
            .btn-secondary {
                @apply bg-secondary text-white hover:bg-secondary/90;
            }
            .animate-fade-in {
                @apply opacity-0 transform translate-y-2;
                animation: fadeIn 0.3s ease-out forwards;
            }
            .animate-fade-out {
                @apply opacity-100 transform translate-y-0;
                animation: fadeOut 0.3s ease-in forwards;
            }
            @keyframes fadeIn {
                to { opacity: 1; transform: translateY(0); }
            }
            @keyframes fadeOut {
                to { opacity: 0; transform: translateY(-10px); }
            }
        }
    </style>
</head>
<body class="bg-gray-50 font-sans text-dark">
<div class="max-w-3xl mx-auto px-4 py-8">
    <!-- 页面标题 -->
    <header class="text-center mb-8">
        <h1 class="text-[clamp(2rem,5vw,3rem)] font-bold text-primary mb-2">Task Manager</h1>
        <p class="text-gray-600">Simple task management with Spring Boot, HTMX and doT.js</p>
    </header>

    <!-- 任务管理卡片 -->
    <div class="bg-white rounded-xl shadow-lg p-6 mb-6">
        <!-- 任务表单 -->
        <form
                id="task-form"
                hx-post="/api/tasks"
                hx-target="#task-list"
                hx-ext="json-enc"
                hx-swap="none"
                hx-on="htmx:afterRequest:fetchTasks()"
                class="flex space-x-3"
        >
            <input
                    type="text"
                    name="title"
                    placeholder="Add a new task..."
                    required
                    class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50"
            >
            <button type="submit" class="btn btn-primary">
                <i class="fa fa-plus mr-2"></i> Add
            </button>
        </form>

        <!-- 任务列表容器 -->
        <div id="task-list" class="space-y-3 mt-4">
            <div class="text-center text-gray-500 py-8">
                <i class="fa fa-spinner fa-spin text-primary text-2xl mb-2"></i>
                <p>Loading tasks...</p>
            </div>
        </div>
    </div>

    <footer class="text-center text-gray-500 text-sm">
        <p>Spring Boot + HTMX + doT.js Task Manager</p>
    </footer>
</div>

<!-- doT.js 任务列表模板 -->
<script type="text/template" id="task-list-template">
    {{~ it.tasks:task:index }}
    <div class="task-item {{? task.completed}}task-item-complete{{?}} animate-fade-in" style="animation-delay: {{= index * 50}}ms">
        <div class="flex items-center space-x-3">
            <button
                    class="task-toggle-btn w-6 h-6 rounded-full border-2 flex items-center justify-center cursor-pointer transition-all
                    {{= task.completed ? 'bg-secondary border-secondary' : 'border-gray-300 hover:border-secondary hover:bg-secondary/10'}}
                    "
                    hx-put="/api/tasks/{{= task.id}}/toggle"
                    hx-target="closest .task-item"
                    hx-swap="none"
                    hx-on="htmx:afterRequest:fetchTasks()"
            >
                <i class="fa fa-check text-white text-xs"></i>
            </button>
            <span class="task-title font-medium">{{= task.title}}</span>
        </div>
        <button
                class="delete-task-btn text-gray-400 hover:text-danger transition-colors"
                hx-delete="/api/tasks/{{= task.id}}"
                hx-target="closest .task-item"
                hx-swap="delete"
                hx-confirm="Are you sure you want to delete this task?"
                hx-on="htmx:beforeRequest:this.closest('.task-item').classList.add('animate-fade-out')"
        >
            <i class="fa fa-trash-o text-lg"></i>
        </button>
    </div>
    {{~}}
    {{? it.tasks.length === 0 }}
    <div class="text-center text-gray-500 py-8">
        <i class="fa fa-check-circle text-secondary text-3xl mb-2"></i>
        <p>No tasks yet. Add your first task!</p>
    </div>
    {{?}}
</script>

<script>
    // 编译 doT.js 模板
    const taskListTemplate = doT.template(document.getElementById('task-list-template').text);

    // 页面加载后获取任务列表
    document.addEventListener('DOMContentLoaded', fetchTasks);

    // 获取任务列表
    function fetchTasks() {
        htmx.ajax('GET', '/api/tasks', {
            handler: function(d,xhr) {
                if (xhr.xhr.status === 200) {
                    try {
                        const tasks = JSON.parse(xhr.xhr.responseText);
                        renderTasks(tasks);
                    } catch (e) {
                        showError('Failed to parse tasks');
                    }
                } else {
                    showError('Failed to load tasks');
                }
            }
        });
    }

    // 渲染任务列表
    function renderTasks(tasks) {
        const taskList = document.getElementById('task-list');
        const html = taskListTemplate({ tasks });
        taskList.innerHTML = html;
        htmx.process(taskList);
    }

    // 显示错误信息
    function showError(message) {
        const taskList = document.getElementById('task-list');
        taskList.innerHTML = `
            <div class="text-center text-danger py-8">
                <i class="fa fa-exclamation-circle text-danger text-2xl mb-2"></i>
                <p>${message}</p>
            </div>
        `;
    }

    // 表单提交后清空输入框
    document.getElementById('task-form').addEventListener('htmx:afterRequest', function() {
        this.reset();
    });
</script>
</body>
</html>

这个前端实现了完整的任务管理界面,包括:

  • 任务列表的展示和动态加载
  • 添加新任务的表单
  • 任务状态的切换
  • 任务的删除功能

所有这些功能都是通过 HTMX 的属性直接实现的,无需编写大量 JavaScript 代码。

当用户执行操作时,HTMX 会自动发送 AJAX 请求到后端 API,并根据响应更新页面。

前后端交互流程

整个应用的交互流程如下:

  1. 页面加载时,HTMX 发送 GET 请求到/api/tasks获取任务列表
  2. 用户提交新任务表单时,HTMX 发送 POST 请求到/api/tasks
  3. 后端创建新任务并返回,HTMX 将新任务添加到列表
  4. 用户点击任务的完成状态时,HTMX 发送 PUT 请求到/api/tasks/{id}/toggle
  5. 后端更新任务状态并返回,HTMX 更新任务列表
  6. 用户删除任务时,HTMX 发送 DELETE 请求到/api/tasks/{id}
  7. 后端删除任务,HTMX 从 DOM 中移除任务项

部署与运行

要运行这个应用,你需要

markdown 复制代码
1.  创建一个 Spring Boot 项目
2.  将上述后端代码复制到 src/main/java/com/example/taskmanager 目录
3.  将前端代码保存至  src/main/resources/static/index.html 
4.  运行 Spring Boot 应用
5.  在浏览器中访问 http://localhost:8080/index.html

总结

通过结合 Spring Boot 和 HTMX,我们实现了一个高效、简洁的前后端分离应用。

这种架构既保留了 Spring Boot 强大的后端处理能力,又通过 HTMX 简化了前端开发,避免了复杂的前端框架和构建流程。

对于中小型项目或者需要快速迭代的应用来说,这种组合是一个非常不错的选择。

如果你正在寻找一种轻量级、高效的 Web 开发解决方案,不妨尝试一下 Spring Boot 与 HTMX 的组合。

相关推荐
BD_Marathon2 小时前
【Flink】部署模式
java·数据库·flink
鼠鼠我捏,要死了捏5 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw5 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友5 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls5 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh5 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫6 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong6 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊7 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉7 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源