۳۰ آذر ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

HTMX برای جاوا با Spring Boot و Thymeleaf

آیا HTMX می تواند چسبی باشد که در نهایت به وعده Ajax محور جاوا عمل می کند؟ بیایید با این برنامه مثال بر اساس HTMX، Spring Boot و Thymeleaf متوجه شویم.

آیا HTMX می تواند چسبی باشد که در نهایت به وعده Ajax محور جاوا عمل می کند؟ بیایید با این برنامه مثال بر اساس HTMX، Spring Boot و Thymeleaf متوجه شویم.

چندی پیش، ما به نحوه ساختن یک برنامه HTMX با جاوا اسکریپت نگاه کردیم. HTMX همچنین با جاوا کار می کند، بنابراین اکنون ما آن را با استفاده از Spring Boot و Thymeleaf امتحان خواهیم کرد. این پشته عالی تمام قدرت و تطبیق پذیری جاوا را با Spring همراه با سادگی مبتکرانه HTMX به شما می دهد.

HTMX: ستاره در حال طلوع

HTMX یک فناوری جدیدتر است که از HTML ساده قدیمی استفاده می کند و به آن قدرت های اضافی مانند تعویض Ajax و DOM می دهد. این در لیست شخصی من از ایده های خوب گنجانده شده است زیرا یک قلمرو کامل از پیچیدگی را از برنامه وب معمولی حذف می کند. HTMX با تبدیل جلو و عقب بین JSON و HTML کار می کند. آن را به عنوان نوعی آژاکس اعلامی در نظر بگیرید.

مصاحبه ای با کارسون گراس خالق HTMX را بخوانید.

جاوا، اسپرینگ و برگ آویشن

در سوی دیگر این معادله جاوا است: یکی از بالغ‌ترین و در عین حال مبتکرانه‌ترین پلتفرم‌های سمت سرور که هیچ کدام را ندارند. Spring یک انتخاب آسان برای افزودن طیف وسیعی از قابلیت های مبتنی بر جاوا است، از جمله پروژه وب بوت بهار برای مدیریت نقاط پایانی و مسیریابی. 

Thymeleaf یک موتور قالب کامل سمت سرور و پیش‌فرض برای Spring Boot Web است. وقتی با HTMX ترکیب می‌شوید، همه چیزهایی را که برای ساختن یک برنامه وب تمام پشته بدون وارد شدن به جاوا اسکریپت نیاز دارید، دارید. 

برنامه نمونه HTMX و جاوا

ما برنامه متعارف Todo را می‌سازیم. به این صورت خواهد بود:

عکس از برنامه نمونه جاوا HTMX-Spring.

کارهای موجود را فهرست می کنیم و اجازه ایجاد موارد جدید، حذف آنها و تغییر وضعیت تکمیل آنها را می دهیم.

نمای کلی

برنامه تکمیل شده Todo روی دیسک به این صورت است:


$ tree
.
├── build.gradle
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── iwjavaspringhtmx
        │               ├── DemoApplication.java
        │               ├── controller
        │               │   └── MyController.java
        │               └── model
        │                   └── TodoItem.java
        └── resources
            ├── application.properties
            ├── static
            │   └── style.css
            └── templates
                ├── index.html
                ├── style.css
                └── todo.html

بنابراین، علاوه بر چیزهای معمولی Gradle، برنامه دارای دو بخش اصلی است که در فهرست /src موجود است: دایرکتوری /main کد جاوا و  را نگه می‌دارد. /resources فایل خصوصیات و دو شاخه فرعی را با قالب‌های CSS و Thymeleaf نگهداری می‌کند.

می‌توانید منبع این پروژه را در مخزن GitHub آن بیابید. برای اجرای آن، به ریشه بروید و $ gradle bootRun را تایپ کنید. سپس می‌توانید از برنامه در localhost:8080 استفاده کنید.

اگر می‌خواهید برنامه را از پایه شروع کنید، می‌توانید با این موارد شروع کنید: $ Spring init --dependencies=web,thymeleaf Spring-htmx. این کار Thymeleaf و Spring Boot را در پروژه Gradle نصب می کند.

مقدمه ای ملایم برای تجزیه و تحلیل کد استاتیک

این برنامه یک برنامه معمولی Spring Boot است که توسط DemoApplication.java اجرا می شود. کد>.

کلاس مدل Java Spring HTMX

اجازه دهید با نگاهی به کلاس مدل خود شروع کنیم: com/example/iwjavaspringhtmx/TodoItem.java. این کلاس مدل سمت سرور است که نشان دهنده یک کار است. در اینجا به نظر می رسد:


public class TodoItem {
  private boolean completed;
  private String description;
  private Integer id;
  public TodoItem(Integer id, String description) {
    this.description = description;
    this.completed = false;
    this.id = id;
  }
  public void setCompleted(boolean completed) {
    this.completed = completed;
  }
  public boolean isCompleted() {
    return completed;
  }
  public String getDescription() {
    return description;
  }
  public Integer getId(){ return id; }
  public void setId(Integer id){ this.id = id; }
  @Override
  public String toString() {
    return id + " " + (completed ? "[COMPLETED] " : "[ ] ") + description;
  }
}

این یک کلاس مدل ساده با گیرنده و تنظیم کننده است. هیچ چیز جالبی نیست، این همان چیزی است که ما می خواهیم.

کلاس کنترلر Java Spring HTMX

در سرور، کنترلر رئیس است. درخواست ها را می پذیرد، منطق را هماهنگ می کند و پاسخ را فرموله می کند. در مورد ما، ما به چهار نقطه پایانی نیاز داریم که برای فهرست کردن موارد، تغییر وضعیت تکمیل آنها، افزودن موارد و حذف آنها استفاده شود. کلاس کنترلر اینجاست:


@Controller
public class MyController {

  private static List<TodoItem> items = new ArrayList();
  static {
    TodoItem todo = new TodoItem(0,"Make the bed");
    items.add(todo);
    todo = new TodoItem(1,"Buy a new hat");
    items.add(todo);
    todo = new TodoItem(2,"Listen to the birds singing");
    items.add(todo);
  }

  public MyController(){ }

  @GetMapping("/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

  @PostMapping("/todos/{id}/complete")
  public String completeTodo(@PathVariable Integer id, Model model) {
    TodoItem item = null;
    for (TodoItem existingItem : items) {
      if (existingItem.getId().equals(id)) {
        item = existingItem;
        break; 
      }
    }
    if (item != null) {
      item.setCompleted(!item.isCompleted());
    }
    model.addAttribute("item",item);
    return "todo"; 
  }

  @PostMapping("/todos")
  public String createTodo(Model model, @ModelAttribute TodoItem newTodo) {
    int nextId = items.stream().mapToInt(TodoItem::getId).max().orElse(0) + 1;
    newTodo.setId(nextId);
    items.add(newTodo);
    model.addAttribute("item", newTodo);
    return "todo";
  }

  @DeleteMapping("/todos/{id}/delete")
  @ResponseBody
  public String deleteTodo(@PathVariable Integer id) {
    for (int i = 0;  i < items.size(); i++) {
      TodoItem item = items.get(i);
      if (item.getId() == id) {
        items.remove(i);
        break;
      }
    }
    return "";
  }
}

متوجه خواهید شد که من به تازگی یک List ثابت ایجاد کرده ام تا موارد را در حافظه نگه دارم. در زندگی واقعی، ما از یک فروشگاه داده خارجی استفاده می کنیم.

برای این تور، چند نقطه جالب دیگر وجود دارد.

ابتدا، نقاط پایانی با @GetMapping، @PostMapping و @DeleteMapping حاشیه نویسی می شوند. این روشی است که شما مسیرهای Spring Web را برای کنترل کننده ها ترسیم می کنید. هر حاشیه نویسی با روش HTTP خود مطابقت دارد (GET، POST، DELETE).

Spring Boot همچنین برداشتن پارامترها از مسیر را با استفاده از حاشیه‌نویسی آرگومان @PathParameter آسان می‌کند. بنابراین، برای مسیر /todos/{id}/delete، @PathVariable Integer id حاوی مقدار در قسمت {id} خواهد بود. مسیر.

در مورد روش createTodo()، آرگومان مشروح @ModelAttribute TodoItem newTodo، به طور خودکار بدنه POST را می گیرد و اعمال می شود. مقادیر آن برای شی newTodo. (این یک راه سریع و آسان برای تبدیل فرم ارسال به یک شی جاوا است.)

در مرحله بعد، از شناسه های آیتم برای دستکاری لیست اقلام استفاده می کنیم. این کرایه استاندارد REST API است.

دو راه برای ارسال پاسخ وجود دارد. اگر حاشیه نویسی @ResponseBody در متد وجود داشته باشد (مانند deleteTodo()) هر آنچه که برگردانده شود به کلمه ارسال خواهد شد. در غیر این صورت، رشته برگشتی به عنوان یک مسیر الگوی Thymeleaf تفسیر می شود (در یک لحظه آن ها را خواهید دید).

آگومان مدل Model خاص است. برای افزودن ویژگی‌ها به محدوده‌ای که به Thymeleaf داده می‌شود، استفاده می‌شود. می‌توانیم روش item زیر را اینگونه تفسیر کنیم: با توجه به درخواست GET به root/path، متغیر آیتم‌ها را به عنوان "itemList" به دامنه اضافه کنید. " و با استفاده از الگوی "index" پاسخی را ارائه دهید.


@GetMapping("/")
  public String items(Model model) {
    model.addAttribute("itemList", items);
    return "index";
  }

در مواردی که در حال رسیدگی به درخواست AJAX هستیم که از قسمت جلویی توسط HTMX ارسال شده است، پاسخ توسط مؤلفه HTMX برای به‌روزرسانی رابط کاربری استفاده می‌شود. به زودی در عمل به این موضوع نگاه خوبی خواهیم داشت.

الگوهای Thymeleaf

اکنون اجازه دهید نگاهی به الگوی شاخص Thymeleaf بیاندازیم. در فایل /resources/templates/index.html زندگی می کند. Spring Boot رشته «index» بازگشتی از روش items() را به این فایل نگاشت استفاده از قراردادها. الگوی index.html ما در اینجا آمده است:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Items List</title>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>Stuff To Do</h1>
      <ul>
        <th:block th:each="item : ${itemList}">
          <th:block th:replace="~{'todo.html'}" th:args="${item}"></th:block>
        </th:block>
      </ul>
      <hr>
      <form hx-post="/todos" th:object="${newTodo}" hx-target="ul" hx-swap="beforeend">
        <input type="text" name="description" placeholder="Add a new item..." required>
        <button type="submit">Add</button>
      </form>
  </body>
</html>

ایده اصلی در Thymeleaf این است که یک ساختار HTML و استفاده از متغیرهای جاوا در آن استفاده کنید. (این معادل استفاده از یک سیستم قالب مانند Pug است.)

Thymeleaf از ویژگی‌ها یا عناصر HTML با پیشوند th: برای نشان دادن جایی که کار خود را انجام می‌دهد استفاده می‌کند. به یاد داشته باشید زمانی که ریشه/مسیر را در کنترلر ترسیم کردیم، متغیر itemList را به محدوده اضافه کردیم. در اینجا، ما از آن در داخل یک th:block با ویژگی th:each استفاده می‌کنیم. ویژگی th:each مکانیسم تکرار کننده در Thymeleaf است. ما از آن برای دسترسی به عناصر itemList استفاده می‌کنیم و هر کدام را به‌عنوان یک مورد با نام متغیر نشان می‌دهیم: item : ${itemList}.

در هر تکرار itemList، رندر را به قالب دیگری واگذار می کنیم. این نوع استفاده مجدد از الگو برای جلوگیری از تکرار کد کلیدی است. خط


<th:block th:replace="~{'todo.html'}"th:args="${item}"></th:block>

به Thymeleaf می‌گوید که الگوی todo.html را ارائه کند و مورد را به‌عنوان آرگومان ارائه کند.

در ادامه به الگوی todo نگاه خواهیم کرد، اما ابتدا توجه کنید که از همان الگو در کنترلر استفاده می‌کنیم، هم در completeTodo و هم در createTodo ، برای ارائه نشانه‌گذاری که در طول درخواست‌های Ajax به HTMX ارسال می‌کنیم. به عبارت دیگر، ما از todo.html به عنوان بخشی از رندر اولیه لیست و ارسال به‌روزرسانی‌ها به UI در طول درخواست‌های Ajax استفاده می‌کنیم. استفاده مجدد از الگوی Thymeleaf ما را خشک نگه می‌دارد.

الگوی انجام کار

اکنون اینجا todo.html است:


<li>
  <input type="checkbox" th:checked="${item.isCompleted}" hx-trigger="click" hx-target="closest li" hx-swap="outerHTML" th:hx-post="|/todos/${item.id}/complete|">
  <span th:text="${item.description}" th:classappend="${item.isCompleted ? 'complete' : ''}"></span>
  <button type="button" th:hx-post="|/todos/${item.id}/delete|" hx-swap="outerHTML" hx-target="closest li">🗑</button>
</li>

می‌بینید که ما یک عنصر فهرست را ارائه می‌کنیم و از یک متغیر item برای پر کردن آن با مقادیر استفاده می‌کنیم. اینجا جایی است که ما به کارهای جالبی با HTMX و Thymeleaf می پردازیم.

ابتدا، ما از th:checked برای اعمال وضعیت علامت‌گذاری شده item.isComplete در ورودی چک باکس استفاده می‌کنیم.

هنگام کلیک بر روی جعبه انتخاب، درخواست Ajax را با استفاده از HTMX به پشتیبان ارسال می‌کنیم:

  1. hx-trigger="click" به HTMX می گوید که Ajax را با یک کلیک راه اندازی کند.
  2. hx-target="closest li" به HTMX می گوید که پاسخ درخواست Ajax را کجا قرار دهد. در مورد ما، می‌خواهیم نزدیک‌ترین عنصر فهرست مورد را جایگزین کنیم. (به یاد داشته باشید که نقطه پایانی حذف ما کل نشانه گذاری مورد فهرست را برای مورد برمی گرداند.)
  3. hx-swap="outerHTML" به HTMX می گوید که چگونه در محتوای جدید، در این مورد، کل عنصر را جایگزین کند.
  4. th:hx-post="|/todos/${item.id}/complete|" به HTMX می گوید که این عنصر فعال Ajax است که یک POST درخواست به URL مشخص شده (نقطه پایانی برای تکمیلTodo ما).

چیزی که در استفاده از Thymeleaf با HTMX باید به آن توجه کرد این است که در نهایت با پیشوندهای مشخصه پیچیده، همانطور که با th:hx-post می‌بینید، مواجه می‌شوید. در اصل، Thymeleaf ابتدا روی سرور اجرا می شود (پیشوند th:) و درون یابی ${item.id} و سپس hx-post را پر می کند. به طور معمول روی مشتری کار می کند.

در مرحله بعد، برای span، فقط متن را از item.description نمایش می دهیم. (توجه کنید که زبان عبارت Thymelef به ما امکان می‌دهد بدون استفاده از پیشوند get به فیلدها دسترسی پیدا کنیم.) همچنین نکته قابل توجه این است که چگونه کلاس سبک تکمیل‌شده را به عنصر span اعمال می‌کنیم. در اینجا چیزی است که CSS ما برای قرار دادن تزئینات خطی روی موارد تکمیل شده استفاده می کند:


th:classappend="${item.isCompleted ? 'complete' : ''}"

این ویژگی Thymeleaf اعمال شرطی یک کلاس بر اساس شرایط بولی مانند item.isComplete را ساده می کند.

دکمه حذف ما به طور مشابه با کادر کامل کار می کند. ما درخواست Ajax را با استفاده از item.id ارائه‌شده توسط Thymeleaf به URL ارسال می‌کنیم و وقتی پاسخ برگشت، مورد فهرست را به‌روزرسانی می‌کنیم. به یاد داشته باشید که ما یک رشته خالی را از deleteTodo() برگرداندیم. بنابراین اثر حذف آیتم لیست از DOM خواهد بود.

شیوه CSS

شیوه CSS در src/main/resources/static/style.css است و چیز قابل توجهی نیست. تنها نکته جالب این است که تزئینات خطی را در spans مدیریت کنید:


span {
  flex-grow: 1;
  font-size: 1rem;
  text-decoration: none;
  color: #333;
  opacity: 0.7;
}

span.complete {
  text-decoration: line-through;
  opacity: 1;
}

نتیجه گیری

ترکیب HTMX، جاوا، اسپرینگ و Thymeleaf دنیایی از امکانات را برای ایجاد تعاملات نسبتاً پیچیده با حداقل مقدار کد دیگ بخار باز می کند. ما می توانیم حجم عظیمی از تعاملات معمولی را بدون نوشتن جاوا اسکریپت انجام دهیم.

در نگاه اول، پشته Java-HTMX به نظر می رسد که سرانجام به وعده Ajax محور جاوا عمل می کند. چیزی شبیه کاری که Google Web Toolkit زمانی تصمیم به انجام آن گرفت. اما بیشتر وجود دارد. HTMX تلاشی برای جهت دهی مجدد برنامه های کاربردی وب به ماهیت واقعی REST، و این پشته راه را به ما نشان می دهد. HTMX آگنوستیک سمت سرور است، بنابراین می‌توانیم آن را بدون مشکل با بک‌اند جاوا خود ادغام کنیم.