個人關注 HTMX 的發展也有一段時間了。最早其實正是因為寫Clojure Script,厭捲了重頭學習React,找React wrapper,並解決這些wrapper的問題,然後在 Clojure 社群上注意到這個名詞。
Htmx 剛出來的時候曾經引起了不小的關注。一些為 Htmx 感到興奮的人覺得:它將前端框架那些複雜的邏輯摒棄,將問題回到網頁開發的基礎。
在學 React 的路上,一個工程師可能從最手簡單的函式元件開始學起。然後學習元件的生命週期、學習怎麼跟後端拿到資料,並且解析後在前端渲染;然後更進一步學習各種 hook,學習如何控制好前端的狀態;學習使用各種編譯工具、學習怎麼整合CSS。以至於最近,React 開始正式推伺服器元件、推非同步的更新……
一個網站真的只能使用這樣的開發方式嗎?
我在寫一些小專案時,常常有這樣的疑惑。
超媒體軀動開發(hypermedia driven design)指的是以超媒體為主進行網頁開發的一種方式。它有以下特徴:
前端大多數的狀態,通常都是屬於從伺服器拿回資料的「伺服器狀態(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&order=desc&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&order=asc&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&order=asc&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 為主的實現相比,複雜度低很多。
一直以來,SPA 的伺服器渲染一直是很難的問題。這些年,React進一步提出了伺服器元件的概念,無疑對前端開發是個巨大的改變,同時,又一次的對原本就已經 複雜的前端框架再上了一次複雜度(我怎麼說又?)
伺服器渲染的好處很簡單、很明顯,在伺服器就把內容先渲染好,有助於網站的SEO,搭配伺服器元件,某些方面可以提升前端的校能。
如果讓後端可以渲杂內容是主要訴求,那直接使用超媒體(HTML)的方式做為溝通的標準,是最簡單的方式。
事實上,這也不是什麼新穎的方式。在SPA出現以前,前後端的溝通就是以超媒體的方式為主。然而過去的網站,很大的一個問題是所有的網站大部份都是靜 態的,因為當時的AJAX並不流行。
現在HTMX只是提供了一組工具擴展了 HTML 的功能,讓開發可以使用聲明式的方式與伺服器溝通,達到非同步渲染(也就是 AJAX) 的效果。
我認為,在大多數的CRUD場景中,HTMX 可以滿足大部份的需求,但卻沒有前端框架那些疊加上去的複雜度。
總結來說,我認為HTMX 會被許多人關注,主要的原因是「簡單」。它偋棄了SPA框架產生的複雜度,回到前端開發的基本(HTML)。透過它提出的工具,在簡單的同 時,又可以達到SPA的一些優點。
htmx 的出現,我相信也可以造成一些老牌MVC框架的複興(因為它解決了傳統MVC框架沒有辦法解決的問題)。然而在這次嘗試HTMX的過程中,我選擇使用冷門的
Clojure + biff。原因如下:
hiccup 做為取代HTML的標注語言。hiccup事實上就是 Clojure 的 Vector 資料結構,我很喜歡這樣的一致性。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 不同的是:
第二點具體怎麼做呢?可以透過一些簡單的scripts達成,如此次使用的 hyperscript
前方講到互動邏輯直接寫在 html 內。會不會違反了程式設計的單一職責原則?最後會不會回到過去的義大利麵式的程式開發?
在編寫超媒體的時候,傾向使用聲明式的方式撰寫。因此,在開發過程中,應該避免直接將複雜的邏輯寫在htmx中。透過撰寫「HDA友好」的scripts, 應該可以避免這個問題。
在前端。任何的改變都是從事件開始。擅用客制化事件,可以讓超媒體軀動的程式碼有更多的互動行為。
而當使用 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>
The islands architecture encourages small, focused chunks of interactivity within server-rendered web pages.
島嶼架構風強調在SSR風格的架構下,將互動的邏輯盡量表成小的、集中的區塊。
在超媒體區動的開發下,搭配事件,可以很有效的將超媒體的控制與互動邏輯分開,就如同上方的sortableJs範例。
最後,在架構上合理的將控制和互動邏輯分開後,有些簡單的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"]]]]]
這次試用 htmx + clojure,最令我感到興奮的地方是,SPA帶來的複雜的抽象層消失了。網頁開發又回到基礎的超媒體控制。我認為這樣的開發模式 在很多專案中可以滅少不必要的開發複雜度,並且更專注在開發需要的業務邏輯上,同時帶來足夠的彈性。
老實說,目前在開源上並沒有找到太多非常成功的使用案例,但在未來如果有機會的話,我會嘗試將這個技術用專案開發上。