htmx:Gopher 走向全棧的完美搭檔?
在傳統的 Web 開發領域,前端和後端開發通常被明確劃分。前端主要負責用戶界面的交互和視覺呈現,運用 HTML、CSS 和 JavaScript 等技術;後端則專注於服務器邏輯、數據庫管理和核心功能實現,常用 Go、Java、PHP、Ruby 等語言。
然而,隨着技術的不斷演進和開發流程的優化,全棧開發逐漸成爲一種趨勢。全棧開發者能夠在項目的不同階段靈活轉換角色,有效降低溝通成本和縮短開發週期。他們對系統的整體架構和工作原理有更深入的理解,從而能更高效地解決問題。此外,全棧技能也使得開發者在就業市場上更具競爭力,能夠承擔更多樣化的職責。
儘管如此,對於許多專注後端的工程師(包括衆多 Gopher)來說,前端開發仍然是一個不小的挑戰。它不僅要求熟悉 JavaScript 等語言,還需要理解複雜的前端框架和工具鏈。這使得不少後端開發者在面對全棧開發時感到力不從心。
幸運的是,技術的進步爲我們提供了更簡單、高效的開發途徑。Go 語言以其簡潔和高效著稱,而 htmx 庫則通過 HTML 屬性實現豐富的前端交互。將兩者結合,開發者可以在無需深入學習 JavaScript 的情況下,輕鬆實現全棧開發。這種組合不僅能夠顯著提升開發效率,還能充分利用服務器端渲染(SSR)的優勢,在性能和用戶體驗方面取得顯著提升。
那麼,htmx 是否真的是 Gopher 走向全棧的完美搭檔呢?在本文中,我們就將探討一下這個問題,介紹一下 htmx 的核心理念和工作原理,並結合代碼示例和使用場景,詳細分析 Go 和 htmx 如何協同工作。至於 Go+htmx 究竟有多能打,相信在本文最後,你會得出自己的評價!
- htmx:爲簡化前端開發而生
傳統的前端開發通常依賴於 JavaScript 框架,例如 React、Vue 或 Angular。這些框架雖然功能強大,但往往伴隨着高昂的學習成本和複雜的開發流程。對於那些主要從事後端開發的程序員來說,學習和掌握這些框架不僅需要花費大量時間,還需要深入理解前端生態系統中的各種概念和工具鏈。這種學習曲線和開發複雜性成爲了許多後端開發者的阻礙,同時也成爲了阻礙 Go 開發者邁向全棧的絆腳石。
htmx 的誕生正是爲了簡化前端開發,特別是對於那些不願意或沒有時間深入學習 JavaScript 的開發者。
htmx 的核心理念是通過擴展 HTML,使其具備更強大的功能,從而減少對 JavaScript 的依賴。它遵循了 "HTML 優先" 的設計原則,允許開發者直接在 HTML 元素中添加特殊的屬性來定義與服務器交互的行爲,比如動態加載、表單處理、局部刷新等,從而實現動態交互,而無需編寫任何 JavaScript 代碼。可以說,htmx 的出現爲後端開發者 (包括 Gopher) 提供了一種新的選擇,使得 Web 應用的開發變得更加直觀和簡便。
不過,htmx 自身卻是一個輕量級的 JavaScript 庫,這與 Go 的設計哲學有些 “異曲同工”,即簡單留給大家,複雜留給自己。作爲 js 庫,它提供了一組簡潔而強大的 API,通過設置 HTML 屬性,開發者就可以實現多種交互功能。以下是 htmx 的一些核心特性:
- 請求類型(hx-get、hx-post、hx-put 和 hx-delete)
通過指定請求類型,htmx 可以在用戶觸發事件時向服務器發送請求,並處理響應。
- 目標更新(hx-target)
支持指定服務器響應數據要插入的 DOM 元素,支持部分頁面更新而無需刷新整個頁面。
- 觸發條件(hx-trigger)
支持定義請求觸發的條件,例如點擊、鼠標懸停、表單提交等事件。
- 交換方式(hx-swap)
支持定義響應內容插入 DOM 的方式,可以選擇替換、插入、刪除等操作。
這些 API 的設計目標是讓開發者能夠通過聲明式的方式來實現前端邏輯,而不必依賴 JavaScript 代碼,以簡化開發過程。
由於幾乎無需後端開發者寫 JavaScript,HTMX 很容易被認爲是 **SSR(服務器端渲染)**的一種實現。它們看似很相似,但它們的思路並不完全一致。SSR 的渲染過程是在服務器上完成的,服務器生成整個 HTML 頁面的內容,並將其發送給客戶端。客戶端接收到完整的 HTML 直接展示給用戶。這也使得 SSR 通常可以提供更快的初始加載體驗,因爲用戶可以立即看到頁面內容,而不必等待 JavaScript 加載和執行。此外,由於 HTML 內容在服務器上渲染,搜索引擎更容易抓取和索引內容。
而 HTMX 的大部分渲染也是在服務端完成的,但它支持在客戶端通過 AJAX 請求動態更新頁面的某些部分,而不需要重新加載整個頁面,只是它是通過簡單的 HTML 屬性 (外加自身 js) 實現這些功能的,而無需用戶手工寫 JavaScript 實現。HTMX 還使得頁面能夠更具交互性,用戶可以在不離開當前頁面的情況下與應用程序進行交互。
因此,htmx 可以視爲一種結合 SSR 和 ** 局部 CSR(客戶端渲染)** 的技術,它讓你通過服務器端渲染 HTML,同時在客戶端實現靈活的動態交互功能。這使得開發者能夠在 SSR 提供的性能優勢和 SEO 友好性基礎上,提升用戶體驗而不必依賴完整的客戶端框架。
雖然保留了 CSR,但與傳統的 JavaScript 框架(如 React、Vue、Angular)相比,htmx 非常輕量,體積非常小,以撰寫本文時的最新 2.0.2 版本 htmx[1] 爲例,它的 js 包大小如下,壓縮版才 10 幾 k:
此外,傳統框架雖然功能強大,但往往需要複雜的配置和較高的學習成本,尤其對於習慣後端開發的開發者來說,更是如此。而使用 HTMX,只需掌握 HTML 和少量的 htmx API 即可開始開發,適合後端開發者快速上手。
說了這麼多 htmx 的優點,那基於 htmx 的開發究竟是怎樣的呢?下面我們就以 htmx 的幾個核心特性爲例,看看如何基於 htmx 開發簡單 web 應用。
- htmx 的基本用法
在前面我們瞭解了 htmx 的幾個核心特性,包括請求類型、目標更新等。下面我們就針對這些核心特性,舉幾個例子,大家初步瞭解一下基於 htmx 的開發 web 應用的流程。
我們先從請求類型開始,瞭解一下基於 htmx 如何向後端發起 POST/GET/PUT/DELETE 等請求。
2.1 示例 1:請求類型
在這第一個示例中,我們使用 Go 語言創建一個簡單的服務器,並使用 htmx 在前端實現不同類型的請求。下面是我們定義的 html 模板,其中包含了 htmx 的自定義屬性:
// go-htmx/demo1/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX Go Example</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<style>
.row {
margin-bottom: 10px;
}
button {
width: 120px;
margin-right: 10px;
}
.result {
display: inline-block;
width: 300px;
border: 1px solid #ccc;
padding: 5px;
min-height: 20px;
}
</style>
</head>
<body>
<h1>HTMX Request Types Demo</h1>
<div class="row">
<button hx-get="/api/get" hx-target="#get-result">GET Request</button>
<span id="get-result" class="result"></span>
</div>
<div class="row">
<button hx-post="/api/post" hx-target="#post-result">POST Request</button>
<span id="post-result" class="result"></span>
</div>
<div class="row">
<button hx-put="/api/put" hx-target="#put-result">PUT Request</button>
<span id="put-result" class="result"></span>
</div>
<div class="row">
<button hx-delete="/api/delete" hx-target="#delete-result">DELETE Request</button>
<span id="delete-result" class="result"></span>
</div>
</body>
</html>
在這個 HTML 模板文件中包含了四個按鈕,每個按鈕對應一種 http 請求類型(GET、POST、PUT、DELETE),具體的實現方式是每個按鈕都使用了相應的 htmx 屬性(hx-get、hx-post、hx-put、hx-delete)來指定請求類型和目標 URL。此外,所有按鈕都使用了 hx-target 來設置服務器的響應將被顯示的元素 id。以 get 請求 button 爲例,響應的值將被放到 id 爲 get-result 的 span 中。
對應的 Go 後端程序就非常簡單了,下面是代碼摘錄:
// go-htmx/demo1/main.go
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
)
func main() {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/api/get", handleGet)
http.HandleFunc("/api/post", handlePost)
http.HandleFunc("/api/put", handlePut)
http.HandleFunc("/api/delete", handleDelete)
fmt.Println("Server is running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
currentDir, _ := os.Getwd()
filePath := filepath.Join(currentDir, "index.html")
http.ServeFile(w, r, filePath)
}
func handleGet(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Received a GET request")
}
func handlePost(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Received a POST request")
}
func handlePut(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Received a PUT request")
}
func handleDelete(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Received a DELETE request")
}
運行該 server 後,用瀏覽器打開 localhost:8080,我們將看到下面頁面:
逐一點擊各個 Button,htmx 會將從服務器收到的響應內容放入對應的 span 中:
2.2 示例 2:觸發條件
在這個示例 2 中,我們將基於 htmx 實現對各種觸發條件的響應與處理,htmx 提供了 hx-trigger 屬性來應對這些不同的事件觸發,包括點擊、鼠標懸停和表單提交等。我們看下面 html 模板代碼:
// go-htmx/demo2/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX Trigger Demo</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<style>
.demo-section {
margin-bottom: 20px;
padding: 10px;
border: 1px solid #ccc;
}
.result {
margin-top: 10px;
padding: 5px;
background-color: #f0f0f0;
min-height: 20px;
}
</style>
</head>
<body>
<h1>HTMX Trigger Demo</h1>
<div class="demo-section">
<h2>Click Trigger</h2>
<button hx-get="/api/click" hx-trigger="click" hx-target="#click-result">
Click me
</button>
<div id="click-result" class="result"></div>
</div>
<div class="demo-section">
<h2>Hover Trigger</h2>
<div hx-get="/api/hover" hx-trigger="mouseenter" hx-target="#hover-result" style="display: inline-block; padding: 10px; background-color: #e0e0e0;">
Hover over me
</div>
<div id="hover-result" class="result"></div>
</div>
<div class="demo-section">
<h2>Form Submit Trigger</h2>
<form hx-post="/api/submit" hx-trigger="submit" hx-target="#form-result">
<input type="text" >
<button type="submit">Submit</button>
</form>
<div id="form-result" class="result"></div>
</div>
<div class="demo-section">
<h2>Custom Delay Trigger</h2>
<input type="text"
hx-get="/api/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-result"
placeholder="Type to search...">
<div id="search-result" class="result"></div>
</div>
</body>
</html>
通過模板代碼,我們可以看到 hx-trigger 的多種用法:
-
點擊觸發(Click Trigger):使用 hx-trigger="click",當按鈕被點擊時觸發請求。
-
懸停觸發(Hover Trigger):使用 hx-trigger="mouseenter",當鼠標懸停在元素上時觸發請求。
-
表單提交觸發(Form Submit Trigger):使用 hx-trigger="submit",當表單提交時觸發請求。
-
自定義延遲觸發(Custom Delay Trigger):使用 hx-trigger="keyup changed delay:500ms",在輸入框中輸入時,等待 500 毫秒後觸發請求。這對於實現搜索建議等功能很有用。
下面是該示例的後端 go 代碼,邏輯非常簡單,針對每個事件調用,簡單返回一個字符串:
// go-htmx/demo2/main.go
... ...
func main() {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/api/click", handleClick)
http.HandleFunc("/api/hover", handleHover)
http.HandleFunc("/api/submit", handleSubmit)
http.HandleFunc("/api/search", handleSearch)
fmt.Println("Server is running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
currentDir, _ := os.Getwd()
filePath := filepath.Join(currentDir, "index.html")
http.ServeFile(w, r, filePath)
}
func handleClick(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Button was clicked!")
}
func handleHover(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You hovered over the element!")
}
func handleSubmit(w http.ResponseWriter, r *http.Request) {
message := r.FormValue("message")
fmt.Fprintf(w, "Form submitted with message: %s", message)
}
func handleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("search")
fmt.Fprintf(w, "Searching for: %s", query)
}
運行該 server 後,用瀏覽器打開 localhost:8080,我們將看到下面頁面:
接下來,我們可以嘗試點擊按鈕、懸停在元素上、提交表單和在搜索框中輸入,看看每個操作如何觸發 HTMX 請求並更新頁面的相應部分,下面是觸發後的結果:
2.3 示例 3:交換方式
在示例 3 中,我們將展示如何使用 htmx 的 hx-swap 屬性實現不同的內容更新方式,包括替換、插入和刪除操作,其中還包含多種替換方式。下面是 html 模板:
// go-htmx/demo3/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX Swap Demo - All Attributes</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<style>
.demo-section {
margin-bottom: 20px;
padding: 10px;
border: 1px solid #ccc;
}
.content-box {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
min-height: 50px;
}
.item {
margin: 5px 0;
padding: 5px;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<h1>HTMX Swap Demo - All Attributes</h1>
<div class="demo-section">
<h2>innerHTML (Default)</h2>
<button hx-get="/api/swap/inner" hx-target="#inner-content">
Swap innerHTML
</button>
<div id="inner-content" class="content-box">
<p>This is the original content. The entire inner HTML will be replaced.</p>
</div>
</div>
<div class="demo-section">
<h2>outerHTML</h2>
<button hx-get="/api/swap/outer" hx-target="#outer-content" hx-swap="outerHTML">
Swap outerHTML
</button>
<div id="outer-content" class="content-box">
<p>This entire div will be replaced, including its container.</p>
</div>
</div>
<div class="demo-section">
<h2>textContent</h2>
<button hx-get="/api/swap/text" hx-target="#text-content" hx-swap="textContent">
Swap textContent
</button>
<div id="text-content" class="content-box">
<p>This <strong>text</strong> will be replaced, but HTML tags will be treated as plain text.</p>
</div>
</div>
<div class="demo-section">
<h2>beforebegin</h2>
<button hx-get="/api/swap/before" hx-target="#before-content" hx-swap="beforebegin">
Insert before
</button>
<div id="before-content" class="content-box">
<p>New content will be inserted before this div.</p>
</div>
</div>
<div class="demo-section">
<h2>afterbegin</h2>
<button hx-get="/api/swap/afterbegin" hx-target="#afterbegin-content" hx-swap="afterbegin">
Insert at beginning
</button>
<div id="afterbegin-content" class="content-box">
<p>New content will be inserted at the beginning of this div, before this paragraph.</p>
</div>
</div>
<div class="demo-section">
<h2>beforeend</h2>
<button hx-get="/api/swap/beforeend" hx-target="#beforeend-content" hx-swap="beforeend">
Insert at end
</button>
<div id="beforeend-content" class="content-box">
<p>New content will be inserted at the end of this div, after this paragraph.</p>
</div>
</div>
<div class="demo-section">
<h2>afterend</h2>
<button hx-get="/api/swap/after" hx-target="#after-content" hx-swap="afterend">
Insert after
</button>
<div id="after-content" class="content-box">
<p>New content will be inserted after this div.</p>
</div>
</div>
<div class="demo-section">
<h2>delete</h2>
<button hx-get="/api/swap/delete" hx-target="#delete-content" hx-swap="delete">
Delete content
</button>
<div id="delete-content" class="content-box">
<p>This content will be deleted when the button is clicked.</p>
</div>
</div>
</body>
</html>
這個示例略複雜,它涵蓋了 hx-swap 的所有屬性:
-
innerHTML(默認):替換目標元素的內部 HTML。
-
outerHTML:用響應替換整個目標元素。
-
textContent:替換目標元素的文本內容,不解析 HTML。
-
beforebegin:在目標元素之前插入響應。
-
afterbegin:在目標元素的第一個子元素之前插入響應。
-
beforeend:在目標元素的最後一個子元素之後插入響應。
-
afterend:在目標元素之後插入響應。
-
delete:刪除目標元素,忽略響應內容。
爲了配合這個演示,我們編寫了一個簡單的 go 後端程序:
// go-htmx/demo3/main.go
... ...
func main() {
http.HandleFunc("/", handleIndex)
http.HandleFunc("/api/swap/inner", handleInner)
http.HandleFunc("/api/swap/outer", handleOuter)
http.HandleFunc("/api/swap/text", handleText)
http.HandleFunc("/api/swap/before", handleBefore)
http.HandleFunc("/api/swap/afterbegin", handleAfterBegin)
http.HandleFunc("/api/swap/beforeend", handleBeforeEnd)
http.HandleFunc("/api/swap/after", handleAfter)
http.HandleFunc("/api/swap/delete", handleDelete)
fmt.Println("Server is running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
currentDir, _ := os.Getwd()
filePath := filepath.Join(currentDir, "index.html")
http.ServeFile(w, r, filePath)
}
func handleInner(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p>This content replaced the inner HTML at %s</p>", time.Now().Format(time.RFC1123))
}
func handleOuter(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<div id=\"outer-content\" class=\"content-box\"><p>This div replaced the entire outer HTML at %s</p></div>", time.Now().Format(time.RFC1123))
}
func handleText(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This replaced the text content at %s. <strong>HTML tags</strong> are not parsed.", time.Now().Format(time.RFC1123))
}
func handleBefore(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p class=\"item\">This content was inserted before the target div at %s</p>", time.Now().Format(time.RFC1123))
}
func handleAfterBegin(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p class=\"item\">This content was inserted at the beginning of the target div at %s</p>", time.Now().Format(time.RFC1123))
}
func handleBeforeEnd(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p class=\"item\">This content was inserted at the end of the target div at %s</p>", time.Now().Format(time.RFC1123))
}
func handleAfter(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p class=\"item\">This content was inserted after the target div at %s</p>", time.Now().Format(time.RFC1123))
}
func handleDelete(w http.ResponseWriter, r *http.Request) {
// For delete, we don't need to send any content back
w.WriteHeader(http.StatusOK)
}
運行該 server 後,用瀏覽器打開 localhost:8080,你應該能看到一個包含八個不同部分的頁面,每個部分演示了 hx-swap 的一種屬性。你可以點擊每個部分的按鈕,觀察內容如何以不同的方式更新或變化。這個綜合示例展示了 hx-swap 的強大功能和靈活性,讓你可以精確控制如何更新頁面的不同部分。下面是你可以看到的效果呈現:
以上就是 htmx 核心屬性的用法,基於這些核心屬性,我們可以實現更多更爲複雜和高級的場景功能。在下一節,我們會舉兩個複雜一些的示例,供大家參考。
- 高級用法
3.1 基於 token 的身份認證
在使用 HTMX 作爲前端與後端進行交互時,通常會涉及到用戶身份認證 [2] 及鑑權 [3],其中一個常見場景是通過前端獲取的 Token(如 JWT)去訪問後端的受保護的 API。下面我們看看使用 HTMX 該如何實現這一常見功能。
下面是網站首頁的 html 模板,包含用戶登錄的 Form:
// go-htmx/demo4/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX Auth Example - Login</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<script>
htmx.on('htmx:afterRequest', function(event) {
if (event.detail.elt.id === 'login-form') {
var xhr = event.detail.xhr;
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
if (response.success) {
localStorage.setItem('auth_token', response.token);
window.location.href = response.redirect;
} else {
document.getElementById('message').innerText = response.message;
}
} else {
document.getElementById('message').innerText = "An error occurred. Please try again.";
}
}
});
</script>
</head>
<body>
<h1>HTMX Auth Example - Login</h1>
<form id="login-form" hx-post="/login" hx-target="#message">
<label for="username">Username:</label>
<input type="text" id="username" required><br><br>
<label for="password">Password:</label>
<input type="password" id="password" required><br><br>
<button type="submit">Login</button>
</form>
<div id="message"></div>
</body>
</html>
這個代碼片段結合了 HTMX 和 JavaScript,處理登錄表單的提交,以及登錄成功後將令牌(Token)存儲到瀏覽器的本地存儲中,並在登錄成功後重定向到 dashboard 頁面。
這段代碼監聽了 HTMX 的 htmx:afterRequest 事件。此事件在 HTMX 請求完成(即請求已經發出並接收到響應)後觸發,event.detail.elt 表示觸發事件的元素。代碼檢查該元素的 id 是否爲 login-form,確認這次請求來自登錄表單。如果是其他表單或元素觸發的請求,它將忽略。如果服務器的身份驗證成功,它以 json 格式返回 token 和重定向地址,前端會解析響應,並將 Token 存儲到本地存儲,然後自動跳轉到登錄後的 dashboard 頁面。
下面是 dashboard 頁面的 html 模板:
// go-htmx/demo4/dashboard.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX Auth Example - Dashboard</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
htmx.on('htmx:configRequest', function(event) {
var token = localStorage.getItem('auth_token');
if (token) {
event.detail.headers['Authorization'] = 'Bearer ' + token;
}
});
});
</script>
</head>
<body>
<h1>Welcome to Your Dashboard</h1>
<button hx-get="/protected" hx-target="#protected-content">Access Protected Content</button>
<div id="protected-content"></div>
</body>
</html>
這段代碼最值得關注的地方就是在後續發出的 Request 中自動加入之前獲取到的 token。這裏是使用了 htmx:configRequest 事件實現的。監聽 HTMX 的 htmx:configRequest 事件,該事件在 HTMX 發出請求之前觸發,它允許你修改即將發出的請求。這裏的 configRequest 的處理邏輯是:如果 Token 存在,將它添加到即將發出的請求的 Authorization 頭中,並格式化爲標準的 Bearer Token 形式(即 "Authorization: Bearer your_token_here")。這樣,後端在處理請求時可以從請求頭中提取出 Token,用於驗證用戶身份。
整個示例的後端 go 程序如下:
// go-htmx/demo4/main.go
package main
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"strings"
"sync"
"github.com/google/uuid"
)
var (
tokens = make(map[string]bool)
tokensMu sync.Mutex
)
type LoginResponse struct {
Success bool `json:"success"`
Token string `json:"token,omitempty"`
Message string `json:"message"`
Redirect string `json:"redirect,omitempty"`
}
func main() {
http.HandleFunc("/", indexHandler)
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/dashboard", dashboardHandler)
http.HandleFunc("/protected", protectedHandler)
fmt.Println("Server is running on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
http.ServeFile(w, r, "index.html")
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
response := LoginResponse{}
if username == "admin" && password == "password" {
token := uuid.New().String()
tokensMu.Lock()
tokens[token] = true
tokensMu.Unlock()
response.Success = true
response.Token = token
response.Message = "Login successful"
response.Redirect = "/dashboard"
} else {
response.Success = false
response.Message = "Login failed. Please check your credentials and try again."
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("dashboard.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.Execute(w, nil)
}
func protectedHandler(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
tokensMu.Lock()
valid := tokens[token]
tokensMu.Unlock()
if !valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, `<div>
<h2>Protected Content</h2>
<p>This is sensitive information only for authenticated users.</p>
<p>Your token: %s</p>
</div>`, token)
}
注:這裏僅是示例,因此只是用了一個 uuid 作爲 token,沒有使用通用的 jwt。
運行程序,登錄並在 Dashboard 中點擊訪問 protected data,我們會看到下面圖中呈現的效果:
下面我們再來看一個略複雜一些的示例,這次我們基於 htmx 來實現 SSE(Server-Sent Event),即服務端事件。
3.2 SSE
Server-Sent Events (SSE) 是一種輕量級的實時通信技術,允許服務器通過 HTTP 協議持續向客戶端推送更新數據。與 WebSocket[4] 不同,SSE 是單向通信,服務器可以推送數據到客戶端,但客戶端無法通過同一連接向服務器發送數據。這種機制非常適合需要頻繁更新數據但對雙向通信要求不高的場景,如股票價格、新聞推送、社交媒體通知等。
htmx 對 SSE 的支持是通過擴展包實現的,下面就是本示例的 index.html 模板代碼:
// go-htmx/demo5/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX SSE Notifications</title>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
</head>
<body>
<h1>實時通知</h1>
<div hx-ext="sse" sse-connect="/events" sse-swap="message">
<ul id="notifications">
<!-- 通知將在這裏動態添加 -->
</ul>
</div>
<script>
htmx.on("htmx:sseMessage", function(event) {
var ul = document.getElementById("notifications");
var li = document.createElement("li");
li.innerHTML = event.detail.message;
ul.insertBefore(li, ul.firstChild);
});
</script>
</body>
</html>
這個代碼片段通過 HTMX 和 Server-Sent Events (SSE) 實現了實時通知的功能。它會動態將服務器端發送的通知添加到頁面的通知列表中。具體來說:
-
hx-ext="sse":啓用了 HTMX 的 SSE 擴展,用於處理 Server-Sent Events(服務器發送事件),使得瀏覽器可以保持與服務器的長連接,實時接收更新。
-
sse-connect="/events":指定了 SSE 連接的 URL。瀏覽器會向 / events 這個路徑發起 SSE 連接,服務器可以通過這個連接持續向客戶端推送消息。
-
sse-swap="message":指示 HTMX 在收到 SSE 消息時觸發事件處理,消息內容將使用 JavaScript 進行處理而不是自動更新 HTML。
-
htmx.on("htmx:sseMessage", function(event)):監聽 HTMX 的 htmx:sseMessage 事件,每當服務器通過 SSE 推送新消息時,該事件會觸發。event.detail.message 包含從服務器接收到的消息內容。
-
var ul = document.getElementById("notifications");:獲取頁面上 ID 爲 notifications 的 < ul > 元素,表示存放通知的容器。收到的通知通過 htmx:sseMessage 事件處理,將消息動態添加到通知列表中,並顯示在網頁上。
下面是示例對應的 Go 後端程序:
// go-htmx/demo5/main.go
func main() {
http.HandleFunc("/", serveHTML)
http.HandleFunc("/events", handleSSE)
fmt.Println("Server starting on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func serveHTML(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html")
}
func handleSSE(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
notificationCount := 1
for {
notification := fmt.Sprintf("新通知 #%d: %s", notificationCount, time.Now().Format("15:04:05"))
fmt.Fprintf(w, "data: <li>%s</li>\n\n", notification)
flusher.Flush()
notificationCount++
time.Sleep(3 * time.Second)
if r.Context().Err() != nil {
return
}
}
}
運行程序,打開瀏覽器訪問 localhost:8080,在加載的頁面中會自動建立 sse 連接,頁面上的通知消息區便會如下面這樣每 3 秒一變化:
不過這個示例的程序有個 “瑕疵”,那就是如果將 htmx 的版本從 1.9.6 換作最新的 2.0.2,那麼示例就將不工作了,翻看了一下 htmx 文檔,應該是 sseMessage 這個 htmx 擴展屬性被刪除了。
如果要讓示例更具通用性,可以將 index.html 換成下面的代碼:
// go-htmx/demo6/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>HTMX SSE Notifications</title>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<style>
#notification {
padding: 10px;
border: 1px solid #ccc;
background-color: #f8f8f8;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>實時通知</h1>
<div id="notification-container">
<div id="notification">等待通知...</div>
</div>
<script>
document.body.addEventListener('htmx:load', function() {
var notificationDiv = document.getElementById('notification');
var evtSource = new EventSource("/events");
evtSource.onmessage = function(event) {
notificationDiv.textContent = event.data;
};
evtSource.onerror = function(err) {
console.error("EventSource failed:", err);
};
});
</script>
</body>
</html>
當然這個代碼更多使用 js 來實現事件的處理。
- 小結
本文探討了 Go 與 htmx 這一全棧組合的簡潔優勢。對於後端開發者而言,這一組合提供了一種無需深入掌握前端技術即可開發現代 Web 應用的高效途徑。
然而,從兩個高級示例中可以看出,JavaScript 代碼仍難以完全避免,雖然數量不多,但在稍複雜的場景下依然不可或缺。
因此,htmx 目前更多被中小型團隊或個人開發者所青睞。這類開發者通常沒有專職的前端人員,但希望快速構建並部署功能完善的 Web 應用。
綜上所述,在我這個對前端開發瞭解甚少的 Go 開發者看來,Go 與 htmx 的組合的確降低了開發門檻,同時提供了性能和 SEO 優勢,使其成爲現代 Web 開發中值得推薦的技術棧之一。不過,對於複雜的 Web 應用,開發者可能需要結合 htmx 和 JavaScript,或更可能直接採用 vue、react 或 angular 等框架。
目前 Go 社區對 htmx 的支持也越來越多,比如 html 模板引擎 templ[5] 可以用於生成 htmx 模板,當然也有專有的 htmx 框架,比如:ghtmx[6]、pagoda[7]、go-htmx[8] 等。
本文涉及的源碼可以在這裏 [9] 下載。
- 參考資料
-
htmx.org[10] - https://htmx.org/
-
htmx sucks[11] - https://htmx.org/essays/htmx-sucks/
-
《HYPERMEDIA SYSTEMS》[12] - https://hypermedia.systems/book/contents/
參考資料
[1]
最新 2.0.2 版本 htmx: https://unpkg.com/browse/htmx.org@2.0.2/dist/
[2]
用戶身份認證: https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/
[3]
鑑權: https://tonybai.com/2023/11/04/understand-go-web-authz-by-example
[4]
WebSocket: https://tonybai.com/2019/09/28/how-to-build-websockets-in-go
[5]
html 模板引擎 templ: https://templ.guide
[6]
ghtmx: https://gitlab.com/go-htmx/go-htmx
[7]
pagoda: https://github.com/mikestefanello/pagoda
[8]
go-htmx: https://github.com/donseba/go-htmx
[9]
這裏: https://github.com/bigwhite/experiments/tree/master/go-htmx
[10]
htmx.org: https://htmx.org/
[11]
htmx sucks: https://htmx.org/essays/htmx-sucks/
[12]
《HYPERMEDIA SYSTEMS》: https://hypermedia.systems/book/contents/
[13]
Gopher 部落知識星球: https://public.zsxq.com/groups/51284458844544
[14]
鏈接地址: https://m.do.co/c/bff6eed92687
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JINnuAODG2uPgwBXulIeZA