1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • 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-RequesthistoryRestoreAsHxRequest: 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ư UnpolyHotwire
  • 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:titlepage: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""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""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
  • listUserssearchUsers đề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-RedirectHX-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: falseresponseHandling 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

    • Thấy tiếc vì khi bàn về HTMX, Hyperscript không nhận được sự chú ý xứng đáng Nó có thể thao tác DOM mà không cần round-trip tới server hay hàm JavaScript riêng, rất hợp với Go + HTMX và có cảm giác như một phần mở rộng của HTML đang làm việc Có thể có người không thích lập trình khai báo, nhưng trong Open Payment Host, nơi xử lý nhiều payment gateway, cả những thao tác DOM phức tạp cũng đã có thể được triển khai chỉ bằng Hyperscript https://hyperscript.org/, https://openpaymenthost.com/
    • Cũng có thể đổi tên thành HUGS stack, ghép HTMX hoặc hypertext·Unix·Go·SQLite
    • Cũng từng thấy Go·Templ·HTMX được gọi là GoTH stack, và nếu thêm sqlc thì thành GoTHs hoặc GHosT Nếu chấp nhận 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ốt
    • Gần đây thường dùng jet cho SQL an toàn kiểu Việc sinh mã cần một database đang chạy, nhưng tôi cũng xem việc nó buộc phải áp dụng migration trước khi viết code là một ưu điểm https://github.com/go-jet/jet
    • Với các web app phản ứng nhỏ như dashboard API thời gian thực, nơi thậm chí có thể không cần database riêng, gần đây tôi hay dùng PAHG template Đây là tổ hợp Pico.css·Alpine.js·HTMX·Go, và tất nhiên cũng có thể kết nối SQLite https://github.com/hiAndrewQuinn/pahg-template
  • HTMX 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

    • Tôi cũng thích HTMX, nhưng khi đưa vào team thì gặp đúng thực tế như vậy Tôi đã đề xuất cho một web app đơn giản, nhưng các developer vốn nhiều năm trả về JSON rồi render bằng JS/jQuery gặp khó khăn ở từng bước khi giải quyết các vấn đề cũ theo cách mới; developer khác thì thậm chí không thử, còn trong team thì cứ liên tục nhắc đến Blazor Dù cách tiếp cận mới nào cũng sẽ khó, nhưng HTMX thực tế không có cơ hội Vì là team nhỏ, nếu tránh được 95% JavaScript và sinh code cùng HTML ở server thì codebase sẽ gọn gàng hơn và kiểm thử cũng dễ hơn, nên thật đáng tiếc. Trong dự án cá nhân tôi vẫn tiếp tục dùng HTMX, đã tìm được luồng làm việc tốt và cảm thấy phát triển web đơn giản hơn rất nhiều
    • Trong vai trò tư vấn, tôi đã chủ động hướng dẫn team và giải thích rằng học HTMX giúp hiểu sâu hơn về trình duyệt, môi trường làm việc thực tế, trong khi React và các công cụ tương tự lại cách ly developer khỏi trình duyệt, nhưng tôi cũng gặp chuyện tương tự: https://www.reddit.com/r/htmx/s/DuXyGgsCWK html/template tốt ở chỗ có bảo mật mặc định và là chuẩn tích hợp sẵn, nhưng do tính động nên có quá nhiều phần không có đáp án cố định; còn Templ thì sau khi viết vài ứng dụng thương mại lớn, tôi thấy trải nghiệm phát triển vẫn chưa thật thuận tiện Vì vậy tôi đã tạo gsx: https://github.com/gsxhq/gsx
    • Không cần clone mỗi lần render template; chỉ cần compile một lần rồi execute Vấn đề trong bài gốc là do tự tạo ra khi cố chỉ định tiêu đề trang từ template chính. Chỉ cần truyền tiêu đề qua context và chia base.tmpl thành BASE_BEGINBASE_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ữ
    • Ở công ty chúng tôi thì hoàn toàn ngược lại: dù có đủ loại vấn đề, gần như không thể thuyết phục rằng React SPA không mở rộng được chỉ vì nó là công nghệ đã được xác lập Họ nhìn từng vấn đề một cách riêng lẻ nhưng không nối được với bức tranh lớn rằng tất cả đều bắt nguồn từ cùng một cấu trúc
  • 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 cũng muốn thận trọng đề xuất SvelteKit Nhìn bên ngoài có vẻ chỉ là một framework frontend JavaScript nữa, nhưng sau khi trực tiếp dùng, tôi thấy nó hỗ trợ rất tốt thiết kế lấy hypertext làm trung tâm mà dòng HTMX hướng tới
  • 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 $state mới đặc biệt tốt

    • Trang HTMX cũng nói rõ điểm đánh đổi rằng nó không phù hợp cho những mục đích như vậy, nhưng để tìm được các bài viết liên quan thì phải đào khá sâu
    • Dự án anh em phía frontend của nhóm phát triển HTMX là Hyperscript xử lý các tính năng như trạng thái frontend. Không biết bạn đã thử dùng nó chưa: https://hyperscript.org/docs/reactivity/
    • Tôi nghĩ cốt lõi của triết lý HTMX ngay từ đầu là đặt câu hỏi liệu ta có thật sự cần các component được nối với nhau bằng trạng thái dùng chung hay không Rốt cuộc thì phần lớn ứng dụng đều thực hiện CRUD trên cơ sở dữ liệu
  • Dạ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

  • Tô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

    • Nếu có chút quan tâm nào đến phát triển web bằng Go, bạn nên đọc cả Let's GoLet's Go Further của Alex Edwards Đây là những cuốn sách nhập môn ngôn ngữ lập trình dễ tiếp cận, thú vị và thực dụng nhất mà tôi từng đọc
  • 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

    • Khi lần đầu tiếp xúc với hypermedia qua Rails, nó tiện và dễ dùng, nên ma sát với Go/HTMX khiến tôi khá bất ngờ; nhưng sau nhiều lần thử, tôi học được rằng trừu tượng hóa rất quan trọng Nếu không có component generator và reverse router thì khá đau đớn, còn Rails cung cấp sẵn nên trước đây không cần bận tâm Ưu điểm lớn nhất là phần tính toán và dữ liệu nằm ở một chỗ. LLM cũng dùng và kiểm thử các ứng dụng hypermedia tự chứa khá tốt, và vì không phải chờ JavaScript nên chu kỳ lặp ngắn hơn Tuy nhiên tôi đồng ý rằng việc không có thư viện component có sẵn để dùng ngay khiến khâu bắt đầu trở nên khó khăn
    • Để dùng HTMX, tôi đã phải thay đổi toàn bộ triết lý thiết kế Không nhất thiết là nhược điểm, có thể còn là thay đổi tốt, nhưng đó là một bước chuyển đáng kể, và kết quả tạo ra gần với hình thái vốn có của web hơn là cố bắt chước ứng dụng di động
    • Tôi tò mò một ví dụ UI cụ thể nào mà Mantine làm được nhưng HTMX thì không
  • Tôi thích Go + Datastar hơn nhiều vì nó đơn giản hơn

    • Go và Datastar hợp nhau như găng tay đặt may Với template thì tôi muốn thêm a-h/templ, và Datastar cũng hỗ trợ nó Thật ngạc nhiên là Datastar lại ít được biết đến đến vậy
    • Tôi cũng thích Datastar hơn, nhưng các tính năng mới của HTMX4 đang thu hẹp khoảng cách khá nhiều Thật vui khi thấy hệ sinh thái đang hội tụ về các pattern tốt