Vì sao tôi chuyển từ HTMX sang Datastar
(everydaysuperpowers.dev)- Khi dùng HTMX, có thể giảm khoảng 70% lượng mã, nhưng lại gặp vấn đề đồng bộ giữa các UI cùng với sự gia tăng độ phức tạp của quản lý trạng thái frontend
- Sau khi áp dụng Datastar, việc phát triển ứng dụng đa người dùng thời gian thực trở nên dễ bảo trì hơn với cấu trúc mã gọn gàng mà không cần WebSockets
- Trong khi HTMX phân tán logic hành vi xoay quanh các thuộc tính HTML, Datastar nâng cao tính nhất quán và khả năng bảo trì của logic thông qua mô hình cập nhật do máy chủ điều khiển
- API của Datastar có ít thuộc tính hơn, mang lại cảm giác tăng độ dễ đọc và năng suất cho mã nguồn
- Datastar tích cực tận dụng các công nghệ web native như Server-Sent Events(SSE), Web Components, CSS View Transitions để hỗ trợ cộng tác thời gian thực và cấu trúc thành phần có thể tái sử dụng
Giới thiệu và động lực
- Năm 2022, tại DjangoCon Europe, David Guillot đã chia sẻ một trường hợp chuyển một SaaS dựa trên React sang HTMX, giúp giảm khoảng 70% lượng mã và cải thiện tính năng
- Sau đó, nhiều đội ngũ đã trải nghiệm việc chuyển từ ứng dụng một trang (SPA) sang ứng dụng hypermedia nhiều trang giúp giảm mã và cải thiện cả trải nghiệm phát triển lẫn trải nghiệm người dùng
- Bản thân tác giả cũng xác nhận rằng khi chuyển dự án từ HTMX sang Datastar, mã ngắn gọn hơn và có thể phát triển ứng dụng đa người dùng thời gian thực mà không cần WebSocket hay quản lý trạng thái phức tạp
Những vấn đề dẫn tới việc chuyển đổi
- Trong lúc chuẩn bị cho bài trình bày tại FlaskCon 2025, tác giả đã cố đồng bộ UI bằng cách kết hợp HTMX với AlpineJS nhưng gặp phải vấn đề đồng bộ giao diện
- Hai thư viện này là các công cụ tách biệt do những nhà phát triển khác nhau tạo ra nên không thể giao tiếp trực tiếp với nhau, buộc lập trình viên phải tự đảm nhận phần tích hợp
- Trong quá trình khởi tạo component ở nhiều thời điểm khác nhau và điều phối sự kiện, lượng mã cần viết và thời gian debug nhiều hơn dự kiến
- Tác giả chú ý tới việc Datastar tích hợp chức năng của cả hai thư viện mà vẫn có kích thước dưới 11KB, nên đã thử dùng
- Điều này có lợi cho hiệu năng tải trang đối với người dùng thiết bị di động
Thiết kế API tốt hơn của Datastar
- API của Datastar mang lại cảm giác nhẹ hơn rất nhiều so với HTMX, và số lượng thuộc tính (attribute) cần thêm để đạt được kết quả mong muốn cũng ít hơn
- HTMX cần nhiều thuộc tính trong hầu hết các tương tác
- Định nghĩa URL, chỉ định phần tử đích và cách xử lý phản hồi đều được cấu hình bằng các thuộc tính riêng
- Thông thường phải dùng 2~3 thuộc tính mỗi lần, đôi khi còn phải lần theo chuỗi kế thừa để hiểu cách thuộc tính hoạt động
<a hx-target="#rebuild-bundle-status-button" hx-select="#rebuild-bundle-status-button" hx-swap="outerHTML" hx-trigger="click" hx-get="/rebuild/status-button"></a> - Datastar thường chỉ cần một thuộc tính duy nhất để triển khai cùng chức năng
<a data-on-click="@get('/rebuild/status-button')"></a>- Ngay cả khi xem lại mã sau vài tháng, vẫn có thể dễ dàng hiểu cách nó hoạt động
Khác biệt trong nguyên lý hoạt động
- Trong khi HTMX là thư viện frontend với mục tiêu mở rộng đặc tả HTML, thì Datastar là thư viện do máy chủ điều khiển, hướng tới xây dựng các ứng dụng cập nhật thời gian thực hiệu năng cao dựa trên web native
- HTMX định nghĩa hành vi bằng cách thêm thuộc tính vào phần tử kích hoạt yêu cầu, và ngay cả khi cập nhật một phần tử ở rất xa trên trang, logic vẫn bị phân tán qua nhiều lớp
- Datastar để máy chủ quyết định cần thay đổi điều gì, từ đó tập trung toàn bộ logic cập nhật vào một chỗ
-
Ví dụ với HTMX
<div> <div id="alert"></div> <button hx-get="/info" hx-select="#info-details" hx-swap="outerHTML" hx-select-oob="#alert"> Get Info! </button> </div>- Khi nhấn nút, một yêu cầu GET được gửi tới
/info, thay thế nút bằng phần tử có IDinfo-detailstrong phản hồi, đồng thời thay thế phần tử có cùng IDalerttrên trang bằng phần tửalerttrong phản hồi - Nút phải biết quá nhiều thông tin, và vì cần biết trước những gì máy chủ sẽ trả về nên nguyên tắc "locality of behavior" của HTMX bị suy yếu
- Khi nhấn nút, một yêu cầu GET được gửi tới
-
Cách tiếp cận được cải thiện của Datastar
<div> <div id="alert"></div> <button id="info-details" data-on-click="@get('/info')"> Get Info! </button> </div>- Máy chủ trả về một chuỗi HTML chứa hai phần tử gốc có cùng ID tương ứng
<p id="info-details">These are the details you are looking for…</p> <div id="alert">Alert! This is a test.</div> - Đây là một lựa chọn đơn giản và có hiệu năng tốt
- Máy chủ trả về một chuỗi HTML chứa hai phần tử gốc có cùng ID tương ứng
Tư duy ở cấp độ component
- Cách tiếp cận tốt hơn là xem HTML như component
- Xác định bản chất của component đó
- Cách người dùng lấy thêm thông tin về một mục cụ thể
- Khi người dùng bấm nút, thông tin sẽ hiện ra, hoặc nếu không có thông tin thì lỗi sẽ được render; dù theo cách nào thì component cũng chuyển sang trạng thái tĩnh
-
Tách component theo từng trạng thái
- Trạng thái placeholder:
<!-- info-component-placeholder.html --> <div id="info-component"> <button data-on-click="@get('/product/{{product.id}}/info')"> Get Info! </button> </div> - Trạng thái hiển thị thông tin:
<!-- info-component-get.html --> <div id="info-component"> {% if alert %}<div id="alert">{{ alert }}</div>{% endif %} <p>{{product.additional_information}}</p> </div> - Khi máy chủ render HTML, Datastar sẽ tự động cập nhật trang
- Tư duy ở cấp độ component giúp ngăn việc rơi vào trạng thái sai hoặc làm mất trạng thái của người dùng
- Trạng thái placeholder:
Cập nhật đồng thời nhiều component
- Một điểm gây ấn tượng trong bài trình bày của David Guillot là khi ứng dụng cập nhật số lượng mục yêu thích, nó đồng thời cập nhật cả component đã thay đổi lẫn phần tử đếm nằm rất xa trên trang
- Với HTMX, điều này đòi hỏi kích hoạt sự kiện JavaScript, rồi từ đó lại kích hoạt component ở xa gửi một yêu cầu GET
- Datastar có thể cập nhật đồng thời nhiều component ngay cả trong một hàm đồng bộ
-
Ví dụ giỏ hàng
- Component thêm vào giỏ hàng:
<form id="purchase-item" data-on-submit="@post('/add-item', {contentType: 'form'})">" > <input type=hidden name="cart-id" value="{{cart.id}}"> <input type=hidden name="item-id" value="{{item.id}}"> <fieldset> <button data-on-click="$quantity -= 1">-</button> <label>Quantity <input name=quantity type=number data-bind-quantity value=1> </label> <button data-on-click="$quantity += 1">+</button> </fieldset> <button type=submit>Add to cart</button> {% if msg %} <p class=message>{{msg}}</p> {% endif %} </form> - Component hiển thị số lượng trong giỏ:
<div id="cart-count"> <svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"> <use href="#shoppingCart"> </svg> {{count}} </div> - Trong Django, có thể cập nhật cả hai component trong cùng một yêu cầu:
from datastar_py.consts import ElementPatchMode from datastar_py.django import ( DatastarResponse, ServerSentEventGenerator as SSE, ) def add_item(request): # important state update omitted return DatastarResponse([ SSE.patch_elements( render_to_string('purchase-item.html', context=dict(cart=cart, item=item, msg='Item added!')) ), SSE.patch_elements( render_to_string('cart-count.html', context=dict(count=item_count)) ), ])
- Component thêm vào giỏ hàng:
Triết lý web native
- Thông qua cộng đồng Datastar trên Discord, tác giả hiểu rằng Datastar không chỉ là một script trợ giúp đơn giản mà là một triết lý xây dựng ứng dụng dựa trên các primitive cốt lõi của web
- Trong khi HTMX cố mở rộng đặc tả HTML, Datastar quan tâm hơn tới việc thúc đẩy áp dụng các tính năng web native
- CSS view transitions
- Server-Sent Events
- Web Components, v.v.
- Tác giả đã đạt được kết quả lớn khi refactor các component AlpineJS phức tạp thành Web Components đơn giản để tái sử dụng ở nhiều nơi
- Đây là một mẫu rất tốt để đạt được tính cục bộ hành vi cao và khả năng tái sử dụng thông qua tạo phần tử HTML tùy biến, ngay cả khi không dùng các công cụ như React
Cập nhật thời gian thực cho ứng dụng đa người dùng
- Những ứng dụng có cộng tác như tính năng hạng nhất sẽ khác biệt so với các ứng dụng khác, và Datastar giải quyết đúng bài toán này
- Phần lớn lập trình viên HTMX hoặc polling để lấy thông tin từ máy chủ, hoặc viết mã WebSocket tùy biến làm tăng độ phức tạp
- Datastar dùng Server-Sent Events(SSE), một công nghệ web đơn giản, để máy chủ "đẩy" cập nhật tới các client đang kết nối
- Khi người dùng thêm bình luận hoặc trạng thái thay đổi, máy chủ cập nhật trình duyệt ngay lập tức với lượng mã bổ sung tối thiểu
- Có thể xây dựng dashboard thời gian thực, bảng quản trị và công cụ cộng tác mà không cần JavaScript tùy biến
- Nếu kết nối client bị gián đoạn, trình duyệt sẽ tự động thử kết nối lại, không cần thêm mã
- Máy chủ cũng có thể được thông báo về "sự kiện nhận được gần nhất"
Tránh độ phức tạp quá mức
- Cộng đồng Datastar trên Discord giúp tác giả hiểu rõ tầm nhìn của Datastar đối với việc xây dựng ứng dụng web
- Cập nhật UI theo kiểu push
- Giảm độ phức tạp
- Xử lý các tình huống cục bộ phức tạp bằng các công cụ như Web Components
- Cộng đồng cũng giúp người dùng mới nhận ra khi họ đang tiếp cận vấn đề theo cách quá phức tạp
Những mẹo chính
- Đừng ngại render lại toàn bộ component rồi gửi đi
- Cách này dễ hơn và không ảnh hưởng lớn tới hiệu năng
- Có thể đạt tỷ lệ nén tốt hơn, và trình duyệt phân tích chuỗi HTML rất nhanh
- Máy chủ là nguồn chân lý của trạng thái và mạnh hơn trình duyệt
- Hãy để máy chủ xử lý phần lớn trạng thái; có thể bạn sẽ không cần nhiều reactive signal như tưởng tượng
- Web Components rất phù hợp để đóng gói logic vào các phần tử tùy biến có tính cục bộ hành vi cao
- Hiệu ứng trường sao trong phần header của website Datastar là một ví dụ điển hình
- Phần tử
<ds-starfield>đóng gói toàn bộ mã cho hoạt ảnh trường sao và lộ ra ba thuộc tính để thay đổi trạng thái bên trong - Datastar điều khiển các thuộc tính đó khi input dạng range thay đổi hoặc khi chuột di chuyển trên phần tử
Khả năng vượt qua giới hạn
- Điều thú vị nhất là tiềm năng mà Datastar mở ra
- Cộng đồng thường xuyên tạo ra các dự án vượt xa những giới hạn mà các nhà phát triển dùng công cụ khác thường gặp phải
Các trường hợp đáng chú ý
- Bản demo giám sát cơ sở dữ liệu trong trang ví dụ
- Tận dụng Hypermedia để cải thiện đáng kể tốc độ và mức dùng bộ nhớ so với bản demo từng được trình bày tại hội nghị JavaScript
- 1 tỷ checkbox của Anders Murphy
- Khi thử nghiệm 1 triệu checkbox vượt quá năng lực máy chủ, anh đã dùng Datastar để triển khai 1 tỷ checkbox trên một máy chủ giá rẻ
- Một ứng dụng web hiển thị dữ liệu từ toàn bộ các trạm radar tại Mỹ
- Khi tín hiệu của radar thay đổi, điểm tương ứng trên UI thay đổi trong vòng dưới 100 mili giây
- Hơn 800.000 điểm được cập nhật mỗi giây, và người dùng có thể tua ngược tới tối đa 1 giờ trước đó với độ trễ dưới 700 mili giây
- Việc điều này khả thi trong một ứng dụng Hypermedia cho thấy những gì Datastar có thể mang lại
Trải nghiệm sử dụng hiện tại
- Tác giả vẫn đang ở giai đoạn khám phá Datastar, và có thể nhanh chóng, dễ dàng triển khai xử lý AJAX cập nhật UI cho các chức năng tiêu chuẩn mà HTMX thường đảm nhiệm
- Đồng thời đang học và thử nghiệm nhiều mẫu khác nhau để đạt được nhiều hơn với Datastar
- Trong nhiều thập kỷ, tác giả luôn quan tâm tới cách mang lại trải nghiệm người dùng tốt hơn bằng cập nhật thời gian thực, và đánh giá cao việc Datastar cho phép cập nhật theo kiểu push ngay cả trong mã đồng bộ
- Khi mới bắt đầu dùng HTMX, tác giả từng cảm thấy rất phấn khích; nhưng sau khi chuyển sang Datastar, không thấy mình mất đi điều gì, mà ngược lại cảm giác như nhận được nhiều hơn rất nhiều
- Nếu bạn từng thấy vui khi dùng HTMX, bạn có lẽ sẽ cảm nhận lại bước nhảy tương tự với Datastar — như thể khám phá lại điều mà web vốn nên làm
2 bình luận
Datastar - framework hypermedia gọn nhẹ để xây dựng ứng dụng web tương tác
Ý kiến trên Hacker News
hx-trigger="click"đi là đã giảm được 20% số thuộc tính. Và nếu viết HTML có tính truy cập tốt hơn, như dùng<button>thay vì<span>, thì sẽ thuyết phục hơn. Cuối cùng, điểm mạnh của Datastar có vẻ là nó đi kèm sẵn các tính năng kiểu Alpine hay Stimulus, và điều đó thực sự rất ấn tượngdata-replace-urlđể URL của view hiện tại tự động cập nhật theo tọa độ tương ứng (x=123&y=456, v.v.)<span hx-target="#rebuild-bundle-status-button" hx-select="#rebuild-bundle-status-button" hx-swap="outerHTML" hx-trigger="click" hx-get="/rebuild/status-button"></span>có vẻ được đổi thành đoạn datastar này:<span data-on-click="@get('/rebuild/status-button')"></span>Và các ví dụ khác còn gây rối hơn nữa. Rốt cuộc tôi không hiểu vì sao người ta lại chuyển từ htmx sang Datastar#rebuild-bundle-status-buttontừ HTML trả về, rồi thay thế phần tử hiện tại”. Còn Datastar thì là “khi click vào span thì làm đúng theo lệnh từ /rebuild/status-button”. Nếu server trả về nhiều phần tử có gắn ID, Datastar sẽ tự nhận diện tất cả và thay thế chúng tương ứng. Tức là không cần dùngtarget,select,swap, chỉ cần có ID là sẽ hoạt động theo ý địnhhtmx-swap-oob="true", nếu không có thì hoạt động sẽ khác kỳ vọng 2. Ngược lại, nếu không phải OOB mà lại cóhtmx-swap-oob="true"thì sẽ bị bỏ qua hoặc hoạt động sai. Vì vậy, khi muốn tái sử dụng cùng một component cho cả OOB và không OOB, server phải luôn trả xuống cờisOob, khá là phiền