De htmx a Django LiveView
Si usas htmx y estás evaluando Django LiveView, este artículo es para ti. Quiero enseñarte rápidamente las equivalencias de que sueles hacer con htmx y cómo lograrlo con Django LiveView. No es una comparación exhaustiva, sino una guía práctica para migrar o decidir cuál usar.
Diferencias fundamentales
Antes de ver código, tenemos que ser claros: arquitectónicamente son muy diferentes. Por no compartir, ni siquiera usan el mismo protocolo de comunicación.
| Característica | htmx | Django LiveView |
|---|---|---|
| Protocolo | HTTP/AJAX | WebSockets |
| Comunicación | Peticiones individuales (GET, POST, etc) | Conexión persistente |
| Estado | Stateless (sin estado) | Stateful (con estado) |
| Actualizaciones en tiempo real | No (polling necesario) | Sí (push del servidor) |
| Requisitos de infraestructura | Mínimos (servidor HTTP) | Moderados (Channels + Redis recomendado) |
| Latencia | Mayor (cada petición HTTP) | Menor (conexión persistente) |
| Soporte para broadcast | No | Sí |
| Rendimiento* | Bueno (16.48ms) | Excelente (9.35ms) |
| Requiere | Vistas o API REST | Django Channels |
- Basado en benchmarks simples de carga con múltiples usuarios.
En términos prácticos, htmx es más sencillo de configurar y usar para interacciones básicas. Django LiveView es más potente para aplicaciones interactivas en tiempo real, cuando necesitas mantener estado en el servidor, hacer broadcast a múltiples clientes o crear aplicaciones tipo SPA.
Instalación y configuración
htmx
<!-- En tu template base -->
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
Listo. Sin configuración de servidor.
Opcionalmente, puedes instalar django-htmx para facilitar el trabajo con htmx en Django. Proporciona middleware que detecta peticiones htmx y simplifica el manejo de headers:
pip install django-htmx
# settings.py
MIDDLEWARE = [
# ... otros middleware
'django_htmx.middleware.HtmxMiddleware',
]
Con esto puedes usar request.htmx en tus vistas para detectar peticiones htmx y acceder a headers específicos.
Django LiveView
Requiere Django Channels y un servidor ASGI como Daphne. Redis no es obligatorio, pero es altamente recomendable ya que si no perderás algunas funcionalidades de comunicación.
Instalación básica (sin Redis, solo para pruebas):
pip install django-liveview channels daphne
Instalación recomendada (con Redis, para desarrollo y producción):
pip install django-liveview channels channels-redis daphne redis
# settings.py
INSTALLED_APPS = [
'daphne', # Debe estar primero
'django.contrib.staticfiles',
# ... otras apps
'liveview',
]
ASGI_APPLICATION = 'myproject.asgi.application'
# Opción 1: Sin Redis (solo pruebas locales)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
# Opción 2: Con Redis (recomendado)
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
},
},
}
# asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from liveview import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(routing.websocket_urlpatterns)
),
})
# urls.py
from django.urls import path, include
urlpatterns = [
path('', include('liveview.urls')),
# ... tus urls
]
Necesitas más paquetes y configuraciones. Pero como verás, la potencia que obtienes a cambio vale la pena.
Caso 1: Actualizar contenido con un clic
El ejemplo más básico. Un botón que actualiza un div.
htmx
<!-- Template -->
<div id="content">Contenido inicial</div>
<button hx-get="/update-content" hx-target="#content">
Actualizar
</button>
# views.py
def update_content(request):
return HttpResponse("<p>Contenido actualizado</p>")
# urls.py
path('update-content/', update_content),
Django LiveView
<!-- Template -->
{% load liveview %}
<!DOCTYPE html>
<html data-liveview-room-uuid="{% liveview_room_uuid %}">
<head>
{% liveview_headers %}
</head>
<body>
<div id="content">{{ content }}</div>
<button data-liveview-function="update_content"
data-action="click->page#run">
Actualizar
</button>
</body>
</html>
# handlers.py
from liveview import liveview_handler
@liveview_handler("update_content")
def update_content(consumer, content):
return {
"target": "#content",
"html": "<p>Contenido actualizado</p>",
}
Observa cómo htmx necesita una ruta específica, mientras LiveView maneja todo via WebSocket con un decorador.
Caso 2: Formulario con validación
Un formulario que valida en el servidor sin recargar la página.
htmx
<!-- Template -->
<form hx-post="/validate-form" hx-target="#errors">
<input type="email" name="email">
<div id="errors"></div>
<button type="submit">Enviar</button>
</form>
# views.py
def validate_form(request):
email = request.POST.get('email')
if not email or '@' not in email:
return HttpResponse('<p style="color:red;">Email inválido</p>')
return HttpResponse('<p style="color:green;">Email válido</p>')
# urls.py
path('validate-form/', validate_form),
Django LiveView
<!-- Template -->
{% load liveview %}
<form data-liveview-function="validate_form"
data-action="submit->page#run">
<input type="email" name="email" id="email-input">
<div id="errors">{{ error_message }}</div>
<button type="submit">Enviar</button>
</form>
# handlers.py
from liveview import liveview_handler
@liveview_handler("validate_form")
def validate_form(consumer, content):
email = content.get('email', '')
if not email or '@' not in email:
error_html = '<p style="color:red;">Email inválido</p>'
else:
error_html = '<p style="color:green;">Email válido</p>'
return {
"target": "#errors",
"html": error_html,
}
Con Django LiveView puedes acceder al estado del formulario directamente desde el objeto content sin tener que hacer request.POST.
Caso 3: Búsqueda en tiempo real
Búsqueda que se actualiza mientras escribes.
htmx
<!-- Template -->
<input type="text"
name="query"
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#results">
<div id="results"></div>
# views.py
def search(request):
query = request.GET.get('query', '')
results = Article.objects.filter(title__icontains=query)[:5]
return render(request, 'search_results.html', {'results': results})
# urls.py
path('search/', search),
El atributo delay:500ms evita hacer peticiones en cada tecla pulsada.
Django LiveView
<!-- Template -->
{% load liveview %}
<input type="text"
name="query"
id="search-input"
data-liveview-function="search"
data-action="input->page#run"
data-liveview-debounce="500">
<div id="results">{% include 'search_results.html' %}</div>
# handlers.py
from django.template.loader import render_to_string
from liveview import liveview_handler
from .models import Article
@liveview_handler("search")
def search(consumer, content):
query = content.get('query', '')
results = Article.objects.filter(title__icontains=query)[:5]
html = render_to_string('search_results.html', {'results': results})
return {
"target": "#results",
"html": html,
}
El atributo data-liveview-debounce="500" cumple la misma función que delay:500ms en htmx.
Caso 4: Actualización automática (polling)
Contenido que se actualiza periódicamente.
htmx
<!-- Template -->
<div hx-get="/stats"
hx-trigger="every 2s"
id="stats">
{{ stats }}
</div>
# views.py
def stats(request):
active_users = get_active_users()
return HttpResponse(f'<p>Usuarios activos: {active_users}</p>')
# urls.py
path('stats/', stats),
Django LiveView
Django LiveView no tiene polling automático del lado del cliente. La filosofía es diferente: el servidor hace broadcast cuando hay cambios. Pero puedes simularlo con un thread en el servidor:
# handlers.py
import threading
from time import sleep
from liveview import liveview_handler
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def poll_stats():
"""Thread que actualiza stats cada 2 segundos"""
channel_layer = get_channel_layer()
while True:
sleep(2)
active_users = get_active_users()
html = f'<p>Usuarios activos: {active_users}</p>'
async_to_sync(channel_layer.group_send)(
'broadcast',
{
'type': 'broadcast_message',
'message': {
'target': '#stats',
'html': html,
}
}
)
# Iniciar thread al arrancar la app
threading.Thread(target=poll_stats, daemon=True).start()
<!-- Template -->
<div id="stats">{{ stats }}</div>
Este enfoque es más potente porque todos los clientes conectados reciben la actualización simultáneamente, sin que cada uno haga polling individual.
Caso 5: Navegación SPA
Navegar sin recargar la página completa.
htmx
<!-- Template base -->
<nav hx-boost="true">
<a href="/about">Sobre nosotros</a>
<a href="/contact">Contacto</a>
</nav>
<div id="content">
<!-- Contenido -->
</div>
# views.py
def about(request):
return render(request, 'about.html')
def contact(request):
return render(request, 'contact.html')
# urls.py
path('about/', about),
path('contact/', contact),
hx-boost="true" convierte enlaces normales en peticiones AJAX. htmx intercepta el clic, hace una petición GET y reemplaza el <body> con el contenido de la respuesta.
Django LiveView
<!-- Template -->
{% load liveview %}
<nav>
<a href="#"
data-liveview-function="load_about"
data-action="click->page#run">
Sobre nosotros
</a>
<a href="#"
data-liveview-function="load_contact"
data-action="click->page#run">
Contacto
</a>
</nav>
<div id="content">
<!-- Contenido -->
</div>
# handlers.py
from django.template.loader import render_to_string
from liveview import liveview_handler
@liveview_handler("load_about")
def load_about(consumer, content):
html = render_to_string('about.html')
return {
"target": "#content",
"html": html,
}
@liveview_handler("load_contact")
def load_contact(consumer, content):
html = render_to_string('contact.html')
return {
"target": "#content",
"html": html,
}
Caso 6: Estado compartido entre usuarios
Múltiples usuarios viendo datos en tiempo real.
htmx
No hay soporte nativo. Necesitas implementar Server-Sent Events (SSE) o WebSockets manualmente, lo cual rompe el modelo de htmx.
Django LiveView
# handlers.py
from liveview import liveview_handler
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
@liveview_handler("add_message")
def add_message(consumer, content):
message_text = content.get('message', '')
channel_layer = get_channel_layer()
# Broadcast a todos los usuarios conectados
async_to_sync(channel_layer.group_send)(
'chat_room',
{
'type': 'broadcast_message',
'message': {
'target': '#messages',
'html': f'<p>{message_text}</p>',
}
}
)
<!-- Template -->
<div id="messages">
<!-- Mensajes aparecen aquí -->
</div>
<form data-liveview-function="add_message"
data-action="submit->page#run">
<input type="text" name="message">
<button type="submit">Enviar</button>
</form>
Todos los usuarios conectados reciben el mensaje instantáneamente.
Caso 7: Manejo de archivos
htmx
<form hx-post="/upload"
hx-encoding="multipart/form-data"
hx-target="#result">
<input type="file" name="file">
<button type="submit">Subir</button>
</form>
<div id="result"></div>
# views.py
def upload(request):
if request.method == 'POST':
uploaded_file = request.FILES['file']
# Procesar archivo
return HttpResponse(f'<p>Archivo {uploaded_file.name} subido</p>')
# urls.py
path('upload/', upload),
Django LiveView
{% load liveview %}
<form data-liveview-function="upload_file"
data-action="submit->page#run"
enctype="multipart/form-data">
<input type="file" name="file" id="file-input">
<button type="submit">Subir</button>
</form>
<div id="result"></div>
# handlers.py
from liveview import liveview_handler
@liveview_handler("upload_file")
def upload_file(consumer, content):
uploaded_file = content.get('file')
if uploaded_file:
# Procesar archivo
return {
"target": "#result",
"html": f'<p>Archivo {uploaded_file.name} subido</p>',
}
Funcionalmente similar, pero sobre WebSocket.
Migración práctica
Si decides migrar de htmx a LiveView:
- Instala las dependencias básicas:
django-liveview,channels,daphne(y opcionalmentechannels-redis+ Redis para producción) - Configura ASGI: Modifica settings.py y asgi.py según la sección de instalación
- Configura channel layers: Usa
InMemoryChannelLayerpara pruebas oRedisChannelLayerpara producción - Convierte vistas a handlers: Cada vista que devuelve HTML parcial se convierte en un handler con
@liveview_handler - Actualiza templates: Reemplaza atributos
hx-*pordata-liveview-*ydata-action. Además agrega los tags necesarios para LiveView
Puedes mantener SSR para algunas páginas, htmx en algunos componentes y usar LiveView en otras. Una tecnología no excluye a la otra.
No hay elección incorrecta. Ambas tecnologías son complementarias y buscan la máxima sencillez dentro de sus paradigmas. Elige la que mejor se adapte a tus necesidades y escala desde ahí. ¡Happy Hacking!
- Diferencias fundamentales
- Instalación y configuración
- htmx
- Django LiveView
- Caso 1: Actualizar contenido con un clic
- htmx
- Django LiveView
- Caso 2: Formulario con validación
- htmx
- Django LiveView
- Caso 3: Búsqueda en tiempo real
- htmx
- Django LiveView
- Caso 4: Actualización automática (polling)
- htmx
- Django LiveView
- Caso 5: Navegación SPA
- htmx
- Django LiveView
- Caso 6: Estado compartido entre usuarios
- htmx
- Django LiveView
- Caso 7: Manejo de archivos
- htmx
- Django LiveView
- Migración práctica
Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.
Comentarios
Todavía no hay ningún comentario.