- Duy trì render phía máy chủ dựa trên
html/template của Go, đồng thời thêm tương tác động bằng HTMX, và từ một renderer duy nhất có thể tùy chọn trả về toàn bộ trang hoặc các mảnh HTML
- Chia markup thành
base.tmpl, template trang và các mảnh tái sử dụng; tận dụng embed.FS của Go 1.16 và tập template dùng chung để đơn giản hóa cấu trúc triển khai và render
- Tùy theo
HX-Request, trả về kết quả tìm kiếm dưới dạng toàn bộ trang hoặc các hàng trong bảng; dùng Vary: HX-Request và historyRestoreAsHxRequest: false để tránh lỗi cache và khôi phục khi quay lại
- Khi chuyển một yêu cầu HTMX sang trang khác, dùng
HX-Redirect cùng phản hồi 2xx thay vì 3xx thông thường; kiểm soát phạm vi thay thế và hiển thị lỗi bằng responseHandling riêng cho 204, 422, 4xx, 5xx
- Lấy cấu hình tắt cache local storage và kế thừa thuộc tính, đồng thời đặt thời gian chờ yêu cầu làm điểm khởi đầu; khi quy mô lớn hơn, có thể thêm template layout trung gian để hỗ trợ cả các cấu trúc màn hình riêng như khu vực quản trị
Vì sao kết hợp render phía máy chủ của Go với HTMX
- HTMX cho phép thêm tương tác động chỉ với rất ít JavaScript, đồng thời vẫn giữ được tính nhất quán và an toàn mà render HTML phía máy chủ của Go
html/template mang lại
- Phần triển khai xoay quanh ba trục
- Cấu trúc template hỗ trợ cả toàn bộ trang lẫn phản hồi HTML một phần
- Redirect và xử lý lỗi trong môi trường HTMX
- Cấu hình thay đổi các giá trị mặc định của HTMX và lý do
- Bắt đầu từ chức năng cơ bản là thay nút bằng hình ảnh, rồi mở rộng thành màn hình tìm kiếm lọc danh sách trong lúc nhập tên người dùng hoặc email
- Mẫu template tương tự không chỉ áp dụng cho HTMX mà còn cho các công cụ HTML-over-the-wire như Unpoly và Hotwire
- Nếu chưa biết cách dùng cơ bản của HTMX, trước tiên nên xem tài liệu chính thức
Thư mục dự án và cấu hình tệp tĩnh
- Dự án được chia thành các khu vực theo vai trò như sau
assets/html/base.tmpl: khung HTML dùng chung cho mọi trang
assets/html/pages/: nội dung từng trang riêng lẻ
assets/html/partials/: các mảnh HTML tái sử dụng ở nhiều nơi
assets/static/css, assets/static/img, assets/static/js: tài sản tĩnh
cmd/web/: mã khởi động server, handler và renderer HTML
- Tạo khung cơ bản bằng các lệnh sau
go mod init example.com/htmx
mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages cmd/web
touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
- HTMX có thể được cài qua CDN hoặc NPM, nhưng thường dùng cách tải một bản sao về và phục vụ trực tiếp như tệp tĩnh của ứng dụng
- Cấu hình đơn giản và tránh được các nhược điểm khi dùng CDN
- Sử dụng
htmx.org@2.0.10 và framework CSS không class bamboo.css@1.4.0
wget -P assets/static/js https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js
wget -P assets/static/css https://cdn.jsdelivr.net/npm/bamboo.css@1.4.0/dist/bamboo.min.css
Tách template cơ sở, trang và một phần
base.tmpl định nghĩa khung tài liệu tổng thể như <head>, tiêu đề chung, <main>, rồi chèn page:title và page:content
- Bamboo CSS và script HTMX được nạp trong template cơ sở; script HTMX được gán thuộc tính
defer
- Trình duyệt tải script song song trong lúc phân tích HTML
- Script được thực thi sau khi quá trình phân tích HTML và xây dựng DOM kết thúc
- Tất cả template không phụ thuộc vào tên tệp mà được đặt tên tường minh bằng
{{define}}...{{end}}
- Mã Go có thể tham chiếu template một cách nhất quán chỉ bằng tên đã định nghĩa
- Dấu
: trong page:title là ký tự phân tách tùy ý; cũng có thể dùng các tên như page_title, page-title, pageTitle, title
- Nút trên trang chủ được chỉ định hai hành vi
hx-get="/gopher": gửi yêu cầu GET /gopher khi nhấp
hx-swap="outerHTML": thay chính nút đó bằng HTML phản hồi
partial:image:gopher là mảnh hình ảnh có thể tái sử dụng, nhận chiều rộng ảnh tại thời điểm gọi thông qua width="{{.}}"
- Các mảnh HTML chỉ dùng trong một trang cụ thể có thể đặt trong tệp trang đó, thay vì thư mục
partials dùng chung
- Đây là cách phân biệt: mảnh dùng ở nhiều trang thì đặt trong thư mục chung, mảnh chỉ dành riêng cho trang thì đặt cạnh nội dung liên quan
Nhúng HTML và tài sản tĩnh vào binary Go
- Sau khi Go 1.16 bổ sung nhúng tệp, có thể đưa HTML và tài sản tĩnh vào binary Go thay vì đọc từ đĩa ở runtime
- Dùng chỉ thị
//go:embed "html" "static" để đưa hai thư mục vào embed.FS, rồi dùng fs.Sub() để tách thành hai hệ thống tệp con
HTMLFiles: hệ thống tệp template dùng html làm gốc
StaticFiles: hệ thống tệp tĩnh dùng static làm gốc
- Việc tách riêng mang lại hai lợi ích
- Mã HTML và mã tệp tĩnh không truy cập không cần thiết vào tệp của nhau
- Khi mở tệp, không cần thêm tiền tố
html/ hoặc static/
- Nếu không muốn dùng
panic() trong sub(), cũng có thể trả về lỗi và khởi tạo hai biến trong main()
- Trong triển khai hiện tại, để
fs.Sub() thất bại thì giá trị thư mục phải là một đường dẫn không hợp lệ; nhưng các chuỗi cố định "html" và "static" vượt qua kiểm tra này, nên rủi ro panic ở runtime là rất thấp
Tập template dùng chung và htmlRenderer
htmlRenderer quản lý luồng render như sau
- Khi khởi động, phân tích cú pháp tập template dùng chung một lần
- Mỗi lần render, clone tập dùng chung
- Phân tích thêm template trang cần thiết cho yêu cầu
- Thực thi template có tên được chỉ định và gửi thành phản hồi HTTP
newHTMLRenderer() đăng ký template.FuncMap rồi phân tích cú pháp các template chung
- Liên kết
now với time.Now
- Các hàm template tùy chỉnh khác cũng có thể được thêm ở cùng vị trí
render() tạo tập template theo từng yêu cầu bằng sharedTemplates.Clone(), và nếu có additionalTemplateFiles thì mở rộng bằng ParseFS()
- Template được thực thi vào
bytes.Buffer trước, sau đó ghi mã trạng thái và gửi làm nội dung phản hồi
- Khi khởi tạo server, phân tích cú pháp
"base.tmpl" và "partials/*.tmpl" thành tập dùng chung
- Template cơ sở và mọi mảnh dùng chung luôn có sẵn trong renderer
- Chỉ template theo từng trang mới được thêm khi handler gọi
- Tệp tĩnh được phục vụ bằng
http.FileServerFS(assets.StaticFiles) và đường dẫn /static/; server chạy ở :5051
Render toàn bộ trang và mảnh HTML
- Handler trang chủ trả về toàn bộ tài liệu bằng lời gọi sau
app.html.render(w, 200, nil, "base", "pages/home.tmpl")
- Đây là lời gọi thêm
pages/home.tmpl vào tập dùng chung và thực thi template base với 200 OK
- Handler
/gopher không phân tích cú pháp tệp bổ sung nào, mà chỉ thực thi partial:image:gopher đã có sẵn trong tập dùng chung
width := 100
app.html.render(w, http.StatusOK, width, "partial:image:gopher")
- HTMX thay thế hình ảnh nhận từ
/gopher cho nút theo thiết lập hx-swap="outerHTML"
- Cấu trúc này mang lại các lợi ích sau
- Template và tài sản tĩnh được nhúng vào binary, giúp triển khai dễ dàng hơn
- Có thể dùng cùng một
render() để trả về cả trang HTML đầy đủ lẫn một đoạn HTML cụ thể
- Giảm trùng lặp markup bằng template cơ sở và template từng phần
- Template từng phần có thể được tái sử dụng trong template cơ sở, nội dung trang và các template từng phần khác
Tìm kiếm người dùng được cập nhật ngay khi nhập
- Chức năng tìm kiếm được chia thành hai route
GET /users: trang HTML đầy đủ chứa thông tin của tất cả người dùng
GET /users/search: đoạn HTML chỉ trả về các hàng trong bảng của những người dùng có tên hoặc email khớp với từ khóa tìm kiếm
- Ô nhập tìm kiếm kết hợp các thuộc tính HTMX sau
- Gửi yêu cầu tìm kiếm bằng
hx-get="/users/search"
- Gửi yêu cầu khi đã qua 500ms sau khi nội dung nhập thay đổi hoặc khi nhấn Enter, bằng
hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
- Từ khóa tìm kiếm được truyền dưới dạng query string như
GET /users/search?query=foo
- Đưa phản hồi vào bên trong
<tbody id="search-results"> bằng hx-target="#search-results"
- Cập nhật thanh địa chỉ và thêm mục vào lịch sử trình duyệt sau mỗi yêu cầu bằng
hx-push-url="true"
users:rows là một template riêng chỉ render các hàng người dùng
- Mỗi hàng hiển thị tên và email
- Nếu
IsGopher là true, render partial:image:gopher với chiều rộng 24
listUsers và searchUsers đều thêm pages/users.tmpl vào tập dùng chung, nhưng template được thực thi thì khác nhau
listUsers: thực thi base để trả về toàn bộ trang
searchUsers: thực thi users:rows để chỉ trả về các hàng khớp
- Nếu từ khóa tìm kiếm rỗng thì dùng toàn bộ danh sách người dùng; nếu có giá trị thì kiểm tra tên và email bằng
strings.Contains()
- Trong ứng dụng thực tế, có thể cân nhắc các cải tiến sau
- Gộp handler danh sách và tìm kiếm thành một
- Tập trung hóa helper xử lý lỗi và logging
- Thêm header Content Security Policy và cơ chế khôi phục panic bằng middleware
- Thiết lập timeout máy chủ phù hợp
Phân biệt trang đầy đủ và phản hồi từng phần bằng HX-Request
- Nếu người dùng mở trực tiếp
/users/search?query=leo trong trình duyệt hoặc truy cập qua liên kết được chia sẻ, họ có thể chỉ thấy phần HTML tương ứng với các hàng trong bảng
- Yêu cầu HTMX luôn bao gồm header
HX-Request: true, nên có thể dựa vào đó để quyết định dạng phản hồi
func isHTMXRequest(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
- Đặt template thực thi mặc định của handler tìm kiếm là
base, và chỉ đổi sang users:rows khi đó là yêu cầu HTMX
- Khi truy cập trực tiếp, người dùng nhận được trang đầy đủ có chứa kết quả tìm kiếm
- Với yêu cầu HTMX, chỉ trả về đoạn các hàng để đưa vào
<tbody>
Header Vary và khôi phục khi quay lại
- Vì cùng một URL trả về các phản hồi khác nhau tùy theo giá trị
HX-Request, cache cũng cần được thông báo về sự khác biệt này
- Thêm
Vary: HX-Request vào mọi phản hồi render
- Về mặt nghiêm ngặt, chỉ thiết lập trong từng handler khi cần thì ít lãng phí hơn
- Thiết lập đồng loạt trong renderer giúp tránh lỗi có thể xảy ra do bỏ sót
- Khi
hx-push-url hoặc hx-boost thêm lịch sử trình duyệt, HTMX sẽ cache HTML của trang đã hoàn chỉnh vào bộ nhớ cục bộ của trình duyệt
- Cache mặc định tối đa 10 trang
- Nếu quay lại tới một điểm không có trong cache, HTMX sẽ yêu cầu lại URL đó từ máy chủ
- Yêu cầu khôi phục mặc định bao gồm
HX-Request: true, nên máy chủ có thể trả về HTML từng phần thay vì trang đầy đủ
- Nếu chọn loại phản hồi bằng
HX-Request, hãy thiết lập historyRestoreAsHxRequest: false
- Loại bỏ
HX-Request: true khỏi yêu cầu khôi phục khi cache miss
- Khi đó máy chủ sẽ trả về trang HTML đầy đủ cho yêu cầu này
- Cấu hình HTMX được đặt trong thẻ meta
htmx-config của base.tmpl
Chuyển hướng yêu cầu HTMX sang trang khác
- HTMX có thể thay thế ngay các đoạn HTML như thông báo thành công, giúp giảm các trường hợp cần luồng Post/Redirect/Get thông thường
- Khi cần đưa người dùng sang một trang hoàn toàn khác, chẳng hạn sau khi đăng nhập thành công thì chuyển đến hồ sơ, không thể xử lý chỉ bằng phản hồi 3xx thông thường
- Trình duyệt đi theo phản hồi 3xx trước HTMX
- HTMX chỉ thấy phản hồi cuối cùng sau khi chuyển hướng và thay thế nội dung đó vào target hiện tại
- Với yêu cầu HTMX, hãy dùng phản hồi 2xx kèm header
HX-Redirect
HX-Redirect: /foo/bar sẽ điều hướng trình duyệt đến URL đó và tải lại toàn bộ trang
- Yêu cầu tới đích điều hướng không bao gồm
HX-Request: true
- Nếu muốn hỗ trợ cả môi trường tắt JavaScript hoặc HTMX chưa được tải, hãy duy trì phản hồi 3xx thông thường cho các yêu cầu không phải HTMX
func redirect(w http.ResponseWriter, r *http.Request, url string, code int) {
if isHTMXRequest(r) {
w.Header().Set("HX-Redirect", url)
w.WriteHeader(http.StatusNoContent)
return
}
http.Redirect(w, r, url, code)
}
- Khi chuyển đến
/profile sau đăng nhập, có thể gọi như sau
redirect(w, r, "/profile", http.StatusSeeOther)
Khác biệt giữa HX-Redirect và HX-Location
HX-Location thực hiện hành vi tương tự chuyển hướng mà không tải lại toàn bộ trang
- Lấy HTML của URL đích
- Thay phản hồi vào
<body>
- Thêm mục mới vào lịch sử trình duyệt
- Khi yêu cầu URL đích,
HX-Request: true sẽ được bao gồm
- Nếu route đích trả về HTML từng phần tùy theo
HX-Request, có thể phát sinh vấn đề
- Handler không thể phân biệt yêu cầu đi theo
HX-Location với yêu cầu HTMX thông thường
- Trường hợp trước cần trang đầy đủ, nhưng trường hợp sau cần phản hồi từng phần
- Khác với khôi phục lịch sử, không có thiết lập nào để tắt
HX-Request: true trong điều hướng bằng HX-Location
- Trong đa số trường hợp, dùng
HX-Redirect sẽ dễ hơn và an toàn hơn, dù phải chấp nhận tải lại toàn bộ trang
- Nếu route đích được cấu hình để luôn trả về HTML đầy đủ thì cũng có thể dùng
HX-Location
Xử lý phản hồi 204·422·4xx·5xx
- Theo mặc định, HTMX không thay thế phản hồi 4xx hoặc 5xx vào DOM
- Giữ nguyên màn hình hiện tại
- Lỗi được ghi vào console của công cụ dành cho nhà phát triển
- Ngay cả khi máy chủ trả về
500 Internal Server Error do tên template không tồn tại, với cấu hình mặc định người dùng cũng không thấy lỗi trên màn hình
- Để hiển thị thông báo lỗi cho người dùng, cấu hình để phản hồi 4xx và 5xx thay thế toàn bộ
<body>
- Ngoại lệ là
422 Unprocessable Content
- Dùng khi trả về form có chứa lỗi xác thực form
- Phản hồi phải được thay thế bình thường vào target HTMX hiện tại
responseHandling được cấu hình theo mã trạng thái như sau
204 No Content: không thay đổi DOM
422 Unprocessable Content: thay thế bình thường vào target hiện tại
- Các 4xx và 5xx còn lại: thay phản hồi vào
<body>
- Các phản hồi khác: thay thế bình thường vào target hiện tại
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
]
- Nếu trong một tương tác cụ thể cần đưa lỗi vào một đích khác thay vì
<body>, có thể ghi đè thiết lập mặc định bằng tiện ích mở rộng response target
Kiểm tra URL hiện tại trên thanh địa chỉ của trình duyệt
- HTMX không thay đổi URL của trình duyệt trong quá trình gửi yêu cầu AJAX và thay thế phản hồi nếu không dùng
hx-boost, hx-push-url, hx-replace-url
- Vì vậy,
r.URL trong handler Go có thể khác với URL mà người dùng thấy trên thanh địa chỉ
- HTMX truyền URL mà trình duyệt đang hiển thị trong header
HX-Current-URL của mỗi yêu cầu
- Hàm trợ giúp sẽ phân tích header này thành
url.URL, và nếu không có header thì trả về r.URL
func browserURL(r *http.Request) (*url.URL, error) {
cu := r.Header.Get("HX-Current-URL")
if cu != "" {
return url.Parse(cu)
}
return r.URL, nil
}
Cấu hình thay đổi giá trị mặc định của HTMX
- Tắt hoàn toàn bộ nhớ đệm trang trong local storage của HTMX bằng
historyCacheSize: 0
- Bộ nhớ đệm trong local storage có thể là nguồn phát sinh lỗi và vấn đề bảo mật
- Khi bấm quay lại, HTML sẽ được lấy lại từ máy chủ thay vì dùng bộ nhớ đệm
- Trong các phiên bản HTMX sau này, tính năng lưu đệm trong local storage cũng sẽ bị tắt theo mặc định vì cùng lý do
- Vô hiệu hóa kế thừa thuộc tính HTMX bằng
disableInheritance: true
- Việc luôn khai báo thuộc tính một cách tường minh sẽ rõ ràng hơn
- Giảm rủi ro lỗi hoặc hành vi ngoài ý muốn
- Trong các phiên bản HTMX sau này, kế thừa thuộc tính cũng sẽ bị tắt theo mặc định
- Dùng
includeIndicatorStyles: false để HTMX không chèn style cho indicator
- Tự định nghĩa style cho indicator cùng với các quy tắc CSS khác để giữ tính nhất quán
- Mặc định, yêu cầu HTMX không có giới hạn thời gian và sẽ chờ cho đến khi máy chủ phản hồi
- Tùy theo cách ứng dụng Go xử lý timeout và deadline, có thể chỉ định
timeout theo mili giây
- Dùng
timeout: 5000 làm điểm khởi đầu
- Cấu hình mặc định cũng bao gồm
historyRestoreAsHxRequest: false và responseHandling theo từng mã trạng thái
<meta
name="htmx-config"
content='{
"includeIndicatorStyles": false,
"historyCacheSize": 0,
"historyRestoreAsHxRequest": false,
"responseHandling":[
{"code":"204", "swap": false},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": true, "target": "body"},
{"code":"...", "swap": true}
],
"timeout": 5000
}'
>
Layout theo từng trang cho ứng dụng lớn
- Nếu chỉ template nền tảng và template theo từng trang là chưa đủ, có thể thêm template layout vào giữa hai tầng này
- Phù hợp với các khu vực cần tiêu đề, menu điều hướng và cấu trúc nội dung khác với trang thông thường, chẳng hạn trang quản trị
base sẽ gọi layout thay vì gọi trực tiếp page:content
<body>
{{template "layout" .}}
</body>
layouts/admin.tmpl định nghĩa cấu trúc chung của khu vực quản trị và gọi page:content bên trong
- Tiêu đề khu vực quản trị
- Menu điều hướng đến người dùng và đơn hàng
<main> chứa nội dung theo từng trang
- Handler truyền cả layout và template trang sẽ dùng vào
render()
app.html.render(
w,
200,
nil,
"base",
"layouts/admin.tmpl",
"pages/admin-orders.tmpl",
)
- Có thể kết hợp layout theo từng đường dẫn và nội dung trang trong khi vẫn giữ template nền tảng dùng chung, nên các màn hình cần cấu trúc riêng như khu vực quản trị cũng có thể mở rộng theo cùng một mẫu render
1 bình luận
Ý kiến trên Hacker News
Thích Go + HTMX, và dùng cùng a-h/templ để tăng tính an toàn kiểu cho template, component và HTML từng phần Toàn bộ bộ công cụ này được gọi là GUS stack, gồm Go·Unix·SQLite, và đã được công bố: https://housecat.com/blog/the-gus-stack-go-unix-sqlite Lấy cảm hứng rất nhiều từ GUTS stack của exe.dev, nhưng dùng HTMX thay vì TypeScript: https://exe.dev/docs/guts Dùng cockroachdb/errors cho lỗi stack trace, templ cho HTML an toàn kiểu, fuego để tạo đặc tả OpenAPI, sqlc để sinh mã SQL, modernc.org/sqlite cho SQLite thuần Go, goose cho migration, dbos cho workflow bền vững, và rod cho kiểm thử Chrome/CDP cùng tự động hóa Sự kết hợp này rất hiệu quả cho cả việc tự viết code hoặc viết với sự hỗ trợ của agent, cũng như quá trình build và triển khai một binary duy nhất
go generate ./...như một bước build thì thu được rất nhiều lợi ích, và cũng dùng goverter để sinh chuyển đổi giữa model của sqlc với object template và giá trị trả về Khoảng 50% boilerplate được tự động sinh, nên tính an toàn kiểu trở nên rất tốtHTMX rất tuyệt cho nhiều mục đích, nhưng nếu đồng đội không đồng thuận thì rất khó đưa vào sử dụng Sẽ liên tục có phản ứng rằng đây không phải công nghệ nghiêm túc, mọi loại bug ban đầu sẽ bị đổ cho HTMX, và dù về sau hóa ra là hiểu lầm thì niềm tin cũng đã bị tổn hại Điều đó xảy ra ngay cả ở đội ngũ tốt nhất mà tôi từng làm việc cùng, và cuối cùng tôi rút ra bài học rằng phải chọn trận để đánh Tôi cũng cho rằng html/template của Go có interface đặc biệt thiếu tự nhiên. Như 《A Philosophy of Software Design》 khuyên, nên đẩy độ phức tạp vào bên trong, nhưng cấu trúc khiến mỗi lần render HTML lại phải bận tâm đến việc clone template là phức tạp không cần thiết
base.tmplthànhBASE_BEGINvàBASE_ENDđể dùng trong template cuối cùng Tiêu đề theo từng trang phải được truyền vào lúc chạy, nhưng cách của bài gốc rốt cuộc cũng sẽ hỏng khi bắt đầu hỗ trợ đa ngôn ngữGần đây tôi dùng HTMX trong một dự án và thấy rất thích Với người biết web trước thời AngularJS và React, điều đặc biệt hay là có thể tạo các trang thật và giảm lượng JavaScript xuống mức tối thiểu JavaScript thuần cũng làm được, nhưng HTMX thay thế phần boilerplate xử lý sự kiện lặp đi lặp lại Nếu không thích triết lý frontend hiện đại thì đáng để thử; các ví dụ đầu tiên trên trang chính thức rất cơ bản, nhưng học thêm một chút sẽ thấy nó mạnh hơn nhiều
Tôi đã thử xây dựng một dự án khá lớn bằng HTMX + Go, nhưng vẫn thấy chưa đủ, và cũng không chắc sau này liệu nó có đạt đến mức mình mong muốn hay không Với các ứng dụng CRUD đơn giản và dashboard quản trị thì rất tuyệt, nhưng khi có nhiều component liên kết với nhau, trạng thái dùng chung và tương tác phức tạp, việc quản lý nhanh chóng trở nên khó khăn Vì ghét React nên tôi chọn HTMX, nhưng cuối cùng vẫn giữ backend bằng Go và chuyển frontend sang SvelteKit ở chế độ SPA; khi tách gọn hai phía, việc phát triển và bảo trì UI phức tạp trở nên dễ hơn nhiều Svelte có cảm giác như một phần mở rộng tự nhiên của HTML hơn là một ngôn ngữ khác như JSX, mô hình quản lý trạng thái và component cũng đơn giản, và cú pháp
$statemới đặc biệt tốtDạo này các dự án phụ của tôi hầu như đều viết bằng Go + HTMX, bất kể có nhờ LLM hỗ trợ hay không Opus và GPT cũng xử lý tổ hợp này rất tốt; có thể làm và khởi động nhanh, lại là một binary duy nhất nên triển khai và hosting cũng tiện Đây là một stack rất tốt cho phát triển lặp nhanh
Nếu dùng HTMX, tôi rất khuyến nghị một cách tạo HTML giúp dễ component hóa bằng cách trích các mảnh HTML dùng chung ở backend thành hàm, giống như React HTMX cần sự linh hoạt trong HTML do backend sinh ra, chẳng hạn tùy tình huống phải gắn hoặc bỏ tag ngay cạnh một đoạn HTML cụ thể. Ví dụ, nếu tìm thấy tag `` ở cấp cao nhất thì nó sẽ cập nhật tiêu đề trang Với các template engine truyền thống dựa trên chuỗi, những việc như vậy khá khó, nhưng với thư viện HTML nhúng trong ngôn ngữ thì đơn giản, lại có thể tận dụng toàn bộ khả năng trừu tượng hóa của ngôn ngữ nên đặc biệt hợp với HTMX Nếu backend là JavaScript, có thể dùng hàm tagged template literal như https://github.com/WebReflection/uhtml-ssr; với Go thì dùng https://www.gomponents.com/; với Scala thì dùng ScalaTags
renderToStaticMarkup, nhưng có lẽ nên chọn một thư viện JSX được viết riêng cho backend thì hơnTôi lần đầu tiếp cận Go qua sách của Alex Edwards và Learn Go with Tests, đến giờ vẫn khuyên đọc Điều đó khiến tôi muốn thử chuyển một số dự án cũ sang HTMX
Tôi đã làm một framework dựa trên Kotlin + HTMX cho mục đích cá nhân: https://github.com/reubenfirmin/zoned Mục tiêu là kiểm tra xem có thể làm toàn bộ web app an toàn kiểu từ đầu đến cuối hay không, và dùng Kotlinx.html để viết HTML bằng một DSL tương tự JSX 90% đầu tiên tôi tự làm, còn 10% cuối và README thì nhờ Claude hỗ trợ để hoàn tất việc công khai
Tôi thích cả Go lẫn Alex Edwards, nhưng kết quả khi dùng HTMX thì lúc nào cũng gây thất vọng Cảm giác độ phức tạp của codebase tăng nhanh gấp đôi so với chính ứng dụng, và luôn gặp các trường hợp biên mà không có lối thoát nếu không dùng mẹo kỳ quặc Tôi hiểu lý do ghét các package Node, nhưng HTMX có vẻ như là một sự bù trừ quá đà cho chuyện đó. Thời gian tiết kiệm được nhờ không phải vật lộn với JSON ít hơn gấp 3 lần thời gian phải bỏ ra để làm cho giao diện và trải nghiệm của ứng dụng tử tế Mantine template có thể cấu hình trong 2 phút để tận dụng các UI component xuất sắc, và nếu nhúng các static asset đã build thì vẫn có thể tạo thành một binary Go duy nhất y hệt: https://github.com/mantinedev/vite-min-template
Tôi thích Go + Datastar hơn nhiều vì nó đơn giản hơn