htmx
clojure

HTMX 使用心得 -- 以 Clojure + htmx 為例

2025-12-16 22:57:4332min

個人關注 HTMX 的發展也有一段時間了。最早其實正是因為寫Clojure Script,厭捲了重頭學習React,找React wrapper,並解決這些wrapper的問題,然後在 Clojure 社群上注意到這個名詞。

Htmx 剛出來的時候曾經引起了不小的關注。一些為 Htmx 感到興奮的人覺得:它將前端框架那些複雜的邏輯摒棄,將問題回到網頁開發的基礎。

在學 React 的路上,一個工程師可能從最手簡單的函式元件開始學起。然後學習元件的生命週期、學習怎麼跟後端拿到資料,並且解析後在前端渲染;然後更進一步學習各種 hook,學習如何控制好前端的狀態;學習使用各種編譯工具、學習怎麼整合CSS。以至於最近,React 開始正式推伺服器元件、推非同步的更新……

一個網站真的只能使用這樣的開發方式嗎?

我在寫一些小專案時,常常有這樣的疑惑。

超媒體軀動開發

超媒體軀動開發(hypermedia driven design)指的是以超媒體為主進行網頁開發的一種方式。它有以下特徴:

  • 前端與後端的溝通不以常見的 JSON 格式溝通,而是以超媒體(HTML) 的方式溝通。
  • 強調聲明式的語法優先於 script。script 為按需撰寫(on demand), 用於優化使用者體驗、效能等等。

伺服器狀態

前端大多數的狀態,通常都是屬於從伺服器拿回資料的「伺服器狀態(server state)」,React 社群為了解決伺服器狀態的快取管理,產生許多很棒的函式庫,如 React Query

然而,這無疑又是對原本就已經很複雜的前端工程又加了一層抽象。面對這些伺服器狀態,如果不能夠直接使用瀏覽器本身能夠讀懂的格式處理,複雜度應該會指數型下降。

<table class="table is-striped is-hoverable is-fullwidth">
    <thead>
        <tr>
            <th>
                <a href="#" hx-get="/demo/data-table-rows?sort=name&amp;order=desc&amp;page=1" hx-target="#data-table-container" hx-swap="innerHTML">
                    Name
                    <span class="icon"><i class="fas fa-sort"></i></span>
                </a>
            </th>
            <th>
                <a href="#" hx-get="/demo/data-table-rows?sort=age&amp;order=asc&amp;page=1" hx-target="#data-table-container" hx-swap="innerHTML">
                    Age
                    <span class="icon"><i class="fas fa-sort"></i></span>
                </a>
            </th>
            <th>
                <a href="#" hx-get="/demo/data-table-rows?sort=role&amp;order=asc&amp;page=1" hx-target="#data-table-container" hx-swap="innerHTML">
                    Role
                    <span class="icon"><i class="fas fa-sort"></i></span>
                </a>
            </th>
        </tr>
    </thead>
    <tbody>
        
        <tr>
            <td>Alice</td>
            <td>28</td>
            <td>Developer</td>
        </tr>
        
        <tr>
            <td>Bob</td>
            <td>34</td>
            <td>Designer</td>
        </tr>
        
        <tr>
            <td>Charlie</td>
            <td>25</td>
            <td>Manager</td>
        </tr>
        
        <tr>
            <td>Eve</td>
            <td>29</td>
            <td>QA</td>
        </tr>
        
    </tbody>
</table>

以上為一個可排序的表格範例來源。與React + JSON 為主的實現相比,複雜度低很多。

伺服器渲染(Server side render) 與伺服器元件(React server component)

一直以來,SPA 的伺服器渲染一直是很難的問題。這些年,React進一步提出了伺服器元件的概念,無疑對前端開發是個巨大的改變,同時,又一次的對原本就已經 複雜的前端框架再上了一次複雜度(我怎麼說又?)

伺服器渲染的好處很簡單、很明顯,在伺服器就把內容先渲染好,有助於網站的SEO,搭配伺服器元件,某些方面可以提升前端的校能。

如果讓後端可以渲杂內容是主要訴求,那直接使用超媒體(HTML)的方式做為溝通的標準,是最簡單的方式。

事實上,這也不是什麼新穎的方式。在SPA出現以前,前後端的溝通就是以超媒體的方式為主。然而過去的網站,很大的一個問題是所有的網站大部份都是靜 態的,因為當時的AJAX並不流行。

現在HTMX只是提供了一組工具擴展了 HTML 的功能,讓開發可以使用聲明式的方式與伺服器溝通,達到非同步渲染(也就是 AJAX) 的效果。

我認為,在大多數的CRUD場景中,HTMX 可以滿足大部份的需求,但卻沒有前端框架那些疊加上去的複雜度。

總結來說,我認為HTMX 會被許多人關注,主要的原因是「簡單」。它偋棄了SPA框架產生的複雜度,回到前端開發的基本(HTML)。透過它提出的工具,在簡單的同 時,又可以達到SPA的一些優點。

About clojure

htmx 的出現,我相信也可以造成一些老牌MVC框架的複興(因為它解決了傳統MVC框架沒有辦法解決的問題)。然而在這次嘗試HTMX的過程中,我選擇使用冷門的 Clojure + biff。原因如下:

  • 我很喜歡 Clojure。Clojure 是一個函數式語言。我相信函數式語言代來的好處,尤其是像超媒體片段管理,我相信函數式語言是個正確的選擇
  • Clojure 社群常使用 hiccup 做為取代HTML的標注語言。hiccup事實上就是 Clojure 的 Vector 資料結構,我很喜歡這樣的一致性。

About hiccup

Hiccup 就是一個表示語言,將 html 使用 clojure 的 vector 語言表示。

考慮以下的 hiccup

(defn blog-title
  "blog-title take a title string and return a title bar div"
  [title]
  [:h2.blog--title (str title)])
  
(blog-title "hello, world")

以上的程式碼,在解析後會變以下的 HTML。

<h2 class=\"blog--title\">hello, world</h2>

當HTML 變成 Clojure 的資料結構後,組織、切分超媒體片段會變成一般的函式切分。因此,可以使用以下的方式撰寫:


;; 略過 blog-unsplash-image, blog-readme-and-divide 函式定義
(defn blog-card
  "[blog-card] is a blog article's breif description.
  which may contain a picture, title and brief of contents."
  [blog-data]
  (let [{:keys [title description url created_at]} blog-data]
    [:div.blog--card (blog-title title)
     [:div.inline-block.blog--meta-gap.mb-4
      (meta-content {:time (to-datetime-string created_at),
                     :time-to-read "3min"})]
     [:div.mb4 (blog-unsplash-image {:url url})] (blog-brief-text description)
     [:div.mb4 (blog-readme-and-divider url)]]))

(defn select-blogs
  "select blogs from database. return a arry which contain all of the blogs"
  [ctx]
  (with-open [ds (db/get-db-connection ctx)]
    #_{:clj-kondo/ignore [:unresolved-var]}
    (db/query ds ["SELECT * from blogs order by created_at desc"])))

(defn blog-table
  [ctx]
  (let [blogs (select-blogs ctx)]
    [:div.container.max-w-screen-lg.mx-auto
     (for [blog blogs] (blog-card blog))]))

這是一個部落格的文章列表,可以看到,它有一些 nextJs 的一些優點(在後端直接訪問資庫並渲染、依照功能將重覆的html 抽成 function等)。 但與 nextjs 不同的是:

  1. 元件就是 HTML。
  2. 不像nextjs 將元件分成 client side and server side。因為它強調聲明式,因此,互動羅輯可以直接寫在 html內。

第二點具體怎麼做呢?可以透過一些簡單的scripts達成,如此次使用的 hyperscript

scripts

前方講到互動邏輯直接寫在 html 內。會不會違反了程式設計的單一職責原則?最後會不會回到過去的義大利麵式的程式開發?

在編寫超媒體的時候,傾向使用聲明式的方式撰寫。因此,在開發過程中,應該避免直接將複雜的邏輯寫在htmx中。透過撰寫「HDA友好」的scripts, 應該可以避免這個問題。

Events

在前端。任何的改變都是從事件開始。擅用客制化事件,可以讓超媒體軀動的程式碼有更多的互動行為。

而當使用 htmx 時,htmx 也提供了一些監體事件,可以讓事件觸發的超媒體改變是由伺服器控制。因此,理論上來說,任何的 javascript 的前端函式庫 ,都有機會整合 HDA,由伺服器控制超媒體。

一個例子來自 sortable.js 的範例整合 htmx

<form class="sortable" hx-post="/items" hx-trigger="end">
  <div class="htmx-indicator">Updating...</div>
  <div><input type='hidden' name='item' value='1'/>Item 1</div>
  <div><input type='hidden' name='item' value='2'/>Item 2</div>
  <div><input type='hidden' name='item' value='3'/>Item 3</div>
  <div><input type='hidden' name='item' value='4'/>Item 4</div>
  <div><input type='hidden' name='item' value='5'/>Item 5</div>
</form>

島嶼(island)

The islands architecture encourages small, focused chunks of interactivity within server-rendered web pages.

島嶼架構風強調在SSR風格的架構下,將互動的邏輯盡量表成小的、集中的區塊。

在超媒體區動的開發下,搭配事件,可以很有效的將超媒體的控制與互動邏輯分開,就如同上方的sortableJs範例。

inline script

最後,在架構上合理的將控制和互動邏輯分開後,有些簡單的script,在HPA架構上鼓勵直接寫 inline script。例如:觸發 class 變化,觸發 動畫特效,呼叫function等等。

有很多的library可以在撰寫 inline script 的時候更友好一點。例如 alpine.js,體感上,它可以用類似angular 或者是 vue 那樣概念的 html 語法,做到 html scripting:

<script src="//unpkg.com/alpinejs" defer></script>
 
<div x-data="{ open: false }">
    <button @click="open = true">Expand</button>
 
    <span x-show="open">
        Content...
    </span>
</div>

又或者是這次使用到的hyperscript。hyperscript 是一個事件軀動的程式語言。它可以用類似自然語言的風格寫script,非常的有趣:

<button _="on click toggle .visible on the next <section/>">
    Show Next Section
</button>
<section>
    ....
</section>

上方範例可以很明顯的看出,這個button 在點擊後,在下一個 section tag內,觸發 visable 這個 class。

即使沒有特別學過hyperscript,程式碼也可以一目了然的看出它做什麼事。

這次搭配clojure,hyperscript變成如以下範例:

[:div.layout--drawer
 [:button.cursor-pointer.m-2
  {:style {:width "1.75rem"} :_ "on click toggle .layout--drawer-open on .layout--drawer"}
  (menu-icon)]
 [:ul.list-none.p-0 {:_ "on click toggle .layout--drawer-open on .layout--drawer"}
  [:li.layout--list-item.flex.items-center [:a.cursor-pointer {:hx-get "/resume"
                                                               :hx-push-url "/"
                                                               :hx-target "#container"} [:span.ml-2 "Resume"]]]
  [:li.layout--list-item.flex.items-center [:a.cursor-pointer {:hx-get "/blogs/table"
                                                               :hx-push-url "/blogs"
                                                               :hx-target "#container"} [:span.ml-2 "Blog"]]]]]

Final word

這次試用 htmx + clojure,最令我感到興奮的地方是,SPA帶來的複雜的抽象層消失了。網頁開發又回到基礎的超媒體控制。我認為這樣的開發模式 在很多專案中可以滅少不必要的開發複雜度,並且更專注在開發需要的業務邏輯上,同時帶來足夠的彈性。

老實說,目前在開源上並沒有找到太多非常成功的使用案例,但在未來如果有機會的話,我會嘗試將這個技術用專案開發上。

參考

  • https://htmx.org/essays/hypermedia-driven-applications/
  • https://htmx.org/essays/hypermedia-friendly-scripting/#islands