Back to "HTMX con partes centrales de ASP.NET: El renacimiento del lado del servidor"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Core HTMX HTMX.NET Web Development

HTMX con partes centrales de ASP.NET: El renacimiento del lado del servidor

Friday, 28 November 2025

Introducción

Si usted ha construido aplicaciones tradicionales ASP.NET Core MVC, usted sabe el problema: que temió "clic flash" cuando los usuarios navegan entre las páginas. Recargas de página completa, el navegador cromado parpadea, el contenido salta alrededor como la nueva página renderiza. Funciona, pero no lo hace sentir moderno.

La técnica de devolver fragmentos HTML parciales del servidor y cambiarlos en el DOM resuelve esto - y no es nuevo. He estado usando este patrón desde los días de jQuery, e incluso antes de eso con vainilla JavaScript y XMLHttpRequest. Lo que ha cambiado es cómo elegante se ha convertido en HTMX.

HTMX nos da una manera declarativa de hacer lo que siempre hemos hecho: devolver HTML renderizado por el servidor y cambiarlo a la página. No más JavaScript personalizado de escritura para cada interacción. No más elegir entre desarrollo del lado del servidor "adecuado" y experiencia de usuario suave. Con HTMX, obtenemos ambos.

Artículo del compañero: Este artículo se centra en el lado de integración ASP.NET Core. Para una inmersión profunda en eventos HTMX, ciclo de vida y extensiones personalizadas, vea mi artículo de acompañamiento: Una gira de silbido de extensiones HTMX y el uso de HTMX con ASP.NET Core.

En este artículo, te mostraré cómo HTMX se integra maravillosamente con ASP.NET Core parcials, cómo el excelente HTMX.NET biblioteca lo hace aún mejor, y cómo mi principalmentelucid.pagingtaghelper El paquete NuGet utiliza HTMX que proporciona una potente paginación con una configuración mínima.

El enfoque es framework-agnóstico también - Django añadió fragmentos de plantilla en la versión 6.0, Rails tiene Turbo Frames, y el ecosistema web más amplio está adoptando patrones HTML-over-the-wire. Es un buen momento para construir aplicaciones de servidor.

¿Qué es HTMX?

HTMX es una biblioteca que le permite acceder a las funciones modernas del navegador directamente desde HTML, en lugar de escribir JavaScript. Se extiende HTML con atributos que le permiten hacer peticiones AJAX, intercambiar contenido y crear interacciones ricas - todo sin dejar su marcado.

Los atributos clave que usará con más frecuencia:

  • hx-get, hx-post, hx-put, hx-delete - Hacer peticiones HTTP
  • hx-target - Especifique dónde poner la respuesta
  • hx-swap - Controlar cómo se intercambia el contenido (innerHTML, outerHTML, etc.)
  • hx-trigger - Definir qué desencadena la petición (clic, cambio, carga, etc.)
  • hx-push-url - Actualizar la URL del navegador sin una recarga completa de la página

Esta es la belleza de esto: sigues escribiendo código del lado del servidor, devolviendo HTML renderizado por el servidor. Sin API JSON, sin plantillas del lado del cliente, sin tuberías de compilación.

Un patrón tan antiguo como AJAX itself

Antes de HTMX, logramos el mismo efecto con mucho más ceremonia. Así es como se veían las actualizaciones parciales en la era de jQuery:

// jQuery circa 2010
$('#load-more').click(function() {
    $.ajax({
        url: '/posts/page/' + currentPage,
        success: function(html) {
            $('#posts-container').append(html);
            currentPage++;
        }
    });
});

E incluso antes, con el JavaScript de vainilla:

// Vanilla JS circa 2005
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        document.getElementById('content').innerHTML = xhr.responseText;
    }
};
xhr.open('GET', '/partial-content', true);
xhr.send();

El patrón del lado del servidor era idéntico: devolver fragmentos HTML, intercambiarlos en el DOM. HTMX simplemente mueve esta lógica de JavaScript a atributos HTML, haciéndolo declarativo, reconocible y mucho menos propenso a errores. La innovación no es la técnica; es la interfaz.

Los desarrolladores de ASP.NET han estado haciendo esto durante años. UpdatePanels en WebForms (2005), PartialView() en MVC desde el primer día, Html.RenderAction() para fragmentos componibles - la capacidad siempre ha estado allí. Lo que nos faltaba era una manera elegante y estandarizada de conectarlo en el lado del cliente. HTMX llena ese hueco perfectamente.

La industria en general también ha estado redescubriendo estos patrones. Términos como "SSR" (Server-Side Rendering), "hybrid rendering", y "islands architecture" están esencialmente describiendo lo que los marcos del lado del servidor siempre han hecho, pero con ojos frescos. Es la validación que la escala de enfoque de servidor-renderizado, realiza, y - con las herramientas adecuadas - proporciona una experiencia de usuario excelente.

Configuración de HTMX en el núcleo de ASP.NET

En primer lugar, incluya HTMX en su diseño. Puede utilizar un CDN o servirlo localmente:

<script src="https://unpkg.com/[email protected]"></script>

No hay paso de construcción, no hay instalación de npm, no hay configuración webpack. Sólo deja una etiqueta de guión y te vas a las carreras.

Alpine.js: El componente cliente-lado

Para la reactividad del lado del cliente (mostrando/ocultando elementos, alternando estados, estado de interfaz de usuario local), Alpine.js A sólo 15KB gzipped, proporciona reactividad declarativa Vue/React-como sin la hinchazón.

<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

Así es como trabajan juntos:

<div x-data="{ open: false }">
    <button x-on:click="open = !open">Toggle</button>
    <div x-show="open" x-transition>
        <button hx-get="/api/data" hx-target="#results">Load Data</button>
    </div>
</div>

Alpine maneja la interfaz de usuario local (la palanca), HTMX maneja las llamadas del servidor (la búsqueda de datos). A lo largo de este artículo, verá este patrón - Alpine para la reactividad del cliente, HTMX para las interacciones del servidor.

Biblioteca HTMX.NET

Khalid Abuhakmeh's HTMX.NET biblioteca proporciona la integración de ASP.NET Core de primera clase. Disponible como Htmx y Htmx.TagHelpers NuGet paquetes, se siente nativo de .NET y hace que trabajar con HTMX sea un placer absoluto. Encontrarás más de Khalid excelente trabajo de código abierto en su GitHub.

Instalación

dotnet add package Htmx
dotnet add package Htmx.TagHelpers

En tu _ViewImports.cshtml:

@addTagHelper *, Htmx.TagHelpers

El método de extensión IsHtmx()

La característica más útil es la Request.IsHtmx() método de extensión, que le indica si la solicitud vino de HTMX. Esto le permite devolver una vista completa o sólo una parcial:

[HttpGet]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20)
{
    var posts = await blogViewService.GetPagedPosts(page, pageSize);

    if (Request.IsHtmx())
        return PartialView("_BlogSummaryList", posts);

    return View("Index", posts);
}

Este patrón es absolutamente brillante. Una sola acción controladora sirve a ambos:

  • Cargas de página completa (cuando los usuarios navegan directamente o actualizan)
  • Actualizaciones parciales (cuando HTMX hace la solicitud)

No hay endpoints API separados, no hay lógica duplicada, no hay serialización de JSON.

Un concepto erróneo común: Muchos desarrolladores de ASP.NET piensan que usted necesita el _ prefijo (como _BlogSummaryList.cshtml) para obtener una representación parcial. ¡Tú no! PartialView() método en sí mismo le dice a ASP.NET Core para saltarse el diseño - está diciendo "olvidar el diseño, sólo renderizar este bit". return PartialView("SearchResults", model) con un archivo de vista regular y funciona perfectamente. La convención de subrayado se originó en ASP.NET Web Pages (WebMatrix) donde se impidió que los archivos se sirvan directamente a través de URL - pero MVC siempre ha protegido todas las vistas de acceso directo de todos modos. Es puramente una convención de nombres para ayudar a identificar vistas Propósito como parciales.

Ayudantes de etiquetas HTMX.NET

HTMX.NET proporciona ayudantes de etiquetas que hacen que trabajar con controladores sea más limpio. En lugar de escribir cadenas de ruta, puede utilizar referencias fuertemente mecanografiadas:

<button
    hx-controller="Comment"
    hx-action="GetCommentForm"
    hx-post
    hx-target="#commentform">
    Reply
</button>

Esto genera la ruta correcta usando el sistema de enrutamiento de ASP.NET Core. Si cambia el nombre de su controlador o acción, su IDE lo captará. ¡Mucho mejor que las cadenas mágicas!

He aquí un ejemplo real del sistema de comentarios de este blog:

<button
    class="btn btn-outline btn-sm mb-4"
    hx-action="Comment"
    hx-controller="Comment"
    hx-post
    hx-vals
    x-on:click.prevent="window.mostlylucid.comments.setValues($event)"
    hx-on="htmx:afterSwap: window.scrollTo({top: 0, behavior: 'smooth'})"
    hx-swap="outerHTML"
    hx-target="#commentform">
    Comment
</button>

Observe cómo HTMX juega bien con Alpine.js (x-on:click.prevent) para aquellos trozos ocasionales de interactividad del lado del cliente.

Otros ayudantes de HTMX.NET

La biblioteca también proporciona:

  • Request.IsHtmxNonBoosted() - Compruebe si es una solicitud HTMX pero no aumentada
  • Request.IsHtmxRefresh() - Compruebe si es una petición de restauración de historial
  • Ayudantes de respuesta para encabezados HTMX (desencadenadores, redireccionamientos, etc.)

Ejemplo del mundo real: Búsqueda con parciales

Aquí está el controlador de búsqueda de este blog, mostrando el patrón de retorno de tres niveles:

[HttpGet]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request", "pagerequest" })]
public async Task<IActionResult> Search(
    string? query,
    int page = 1,
    int pageSize = 10,
    [FromHeader] bool pagerequest = false)
{
    var searchModel = await BuildSearchModel(query, page, pageSize);

    if (pagerequest && Request.IsHtmx())
        return PartialView("_SearchResultsPartial", searchModel.SearchResults);

    if (Request.IsHtmx())
        return PartialView("SearchResults", searchModel);

    return View("SearchResults", searchModel);
}

Tres vías de retorno para tres escenarios:

  1. Solicitudes de paginación - mínimo parcial (sólo la lista de resultados)
  2. Cambios de filtro - sección parcial (resultados con filtros)
  3. Navegación directa - página completa (distribución + todo)

El punto de vista parcial (_SearchResultsPartial.cshtml) utiliza el ayudante de la etiqueta de paginación:

@model Mostlylucid.Models.Blog.PostListViewModel
<div class="pt-2" id="content">
    @if (Model.Data?.Any() is true)
    {
        <div class="inline-flex w-full items-center justify-center pb-4">
            @if (Model.TotalItems > Model.PageSize)
            {
                <pager
                    x-ref="pager"
                    link-url="@Model.LinkUrl"
                    hx-boost="true"
                    hx-target="#content"
                    hx-swap="show:none"
                    page="@Model.Page"
                    page-size="@Model.PageSize"
                    total-items="@Model.TotalItems"
                    hx-headers='{"pagerequest": "true"}'>
                </pager>
            }
        </div>
        @foreach (var post in Model.Data)
        {
            <partial name="_ListPost" model="post"/>
        }
    }
</div>

Romper el ayudante de la etiqueta del buscapersonas:

  • hx-boost="true" - Intercepta enlaces, convierte a AJAX
  • hx-target="#content" - Dónde inyectar la respuesta
  • hx-headers='{"pagerequest": "true"}' - Encabezado personalizado le dice al controlador que es paginación
  • El controlador comprueba Request.IsHtmx() && pagerequest para devolver sólo el mínimo parcial

El paquete sobre todolucid.pagingtaghelper

Escribí principalmentelucid.pagingtaghelper para evitar el código de paginación repetitivo. Es HTMX-primero pero funciona sin JavaScript también.

Instalación

dotnet add package mostlylucid.pagingtaghelper

Añadir a _ViewImports.cshtml:

@addTagHelper *, mostlylucid.pagingtaghelper

Características principales

Aplicación IPagingModel<T> y has terminado:

public class BasePagingModel<T> : IPagingModel<T> where T : class
{
    public int Page { get; set; }
    public int TotalItems { get; set; }
    public int PageSize { get; set; }
    public string LinkUrl { get; set; }
    public List<T> Data { get; set; }
}

Lo que obtienes:

  • Se requiere configuración cero
  • Múltiples marcos de interfaz de usuario (TailwindCSS + DaisyUI, Bootstrap 5, vistas personalizadas)
  • Soporte de modo oscuro
  • 8 idiomas incorporados
  • Cabeceras clasificables, selectores de tamaño de página
  • Mejora progresiva (funciona sin JavaScript)
  • Soporte token de continuación para bases de datos NoSQL

El ayudante de etiquetas genera enlaces que preservan las cadenas de consulta, soportan cabeceras personalizadas e integran perfectamente con HTMX (vea el ejemplo anterior).

Diagrama de flujo HTMX

Así es como todo el sistema encaja:

graph TB
    A[Browser] -->|"Initial page load"| B[Controller Action]
    B -->|"Request.IsHtmx() = false"| C[Return Full View]
    C --> D[Render Layout + Partial]

    A -->|"User clicks pagination/filter"| E[HTMX Request]
    E -->|"hx-get with headers"| F[Same Controller Action]
    F -->|"Request.IsHtmx() = true"| G{Check Headers}
    G -->|"pagerequest header"| H[Return Minimal Partial]
    G -->|"No special header"| I[Return Section Partial]

    H --> J[Swap Content in Target]
    I --> J

    J -->|"User clicks another link"| E

    style B stroke:#333,stroke-width:2px
    style F stroke:#333,stroke-width:2px
    style C stroke:#0066cc,stroke-width:2px
    style H stroke:#00cc66,stroke-width:2px
    style I stroke:#00cc66,stroke-width:2px

Comparando con otros marcos

Fragmentos de plantilla de Django

Django añadió la representación de plantilla parcial adecuada en la versión 6.0 (diciembre 2024) con fragmentos de plantilla. Antes de eso, los desarrolladores de Django normalmente usaban etiquetas de inclusión o paquetes de terceros como django-render-block. PartialView() desde la versión 1.0 en 2016 - diferentes marcos, diferentes líneas de tiempo, pero el mismo destino: fragmentos HTML para HTMX.

Turboframes de carriles

Ruby on Rails tiene Turbo Frames (parte de Hotwire), que es similar en espíritu:

<%= turbo_frame_tag "posts" do %>
  <%= render @posts %>
<% end %>

La diferencia es que Turbo requiere marcadores de marco específicos tanto en la solicitud como en la respuesta. HTMX es más flexible - cualquier punto final puede devolver cualquier HTML, y usted decide dónde va con hx-target.

Phoenix LiveView

Phoenix LiveView de Elixir adopta un enfoque diferente con conexiones persistentes de WebSocket y estado del lado del servidor:

def handle_event("load_more", _params, socket) do
  {:noreply, assign(socket, posts: load_more_posts())}
end

LiveView es brillante para aplicaciones en tiempo real, pero requiere infraestructura WebSocket y memoria del servidor para las conexiones. HTMX utiliza HTTP antiguo - apátrida, caché, escalable. Para un blog, eso es perfecto.

Consideraciones sobre el desempeño

Caché de salida: El OutputCache atributo varía según hx-request encabezado, en caché páginas completas y parciales por separado:

[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request", "pagerequest" })]

Eficiencia de la red: HTML renderizado por servidor es a menudo más pequeño que JSON + plantillas del lado del cliente, requiere menos viajes de ida y vuelta, y cachés correctamente.

Tamaño del paquete: HTMX (14KB) + opcional Alpine.js (15KB) + ayudante de etiqueta de paginación (0KB, lado servidor) = inferior a 30KB total. Compare eso con una aplicación típica de React (200KB+).

Patrones avanzados

Actualizaciones optimistas de la interfaz de usuario - Combine HTMX y Alpine para una retroalimentación instantánea:

<div x-data="{ count: @Model.CommentCount }">
    <button hx-post="/comment/like" x-on:click="count++" hx-on::after-request="count = $event.detail.xhr.response">
        Likes: <span x-text="count"></span>
    </button>
</div>

El conteo se actualiza inmediatamente (optimista), luego se sincroniza con la respuesta del servidor.

Swaps fuera de banda - Actualizar varias secciones de página de una respuesta:

<div id="main-content"><!-- Main response --></div>
<div id="notification-count" hx-swap-oob="true"><span>5 new</span></div>

Perfecto para tarjetas de notificación, recuentos de carros, etc. Cubro este patrón en profundidad en Mostrar tostada y intercambio con HTMX.

Plantillas del lado del cliente con WebAPI - A veces quieres una experiencia intermedia: HTML renderizado por servidor para la mayoría de las cosas, pero JSON de un WebAPI para contenido dinámico específico. extensión cliente-side-templates le permite hacer exactamente esto:

<script src="https://unpkg.com/[email protected]/client-side-templates.js"></script>
<script src="https://unpkg.com/mustache@latest"></script>

<div hx-ext="client-side-templates">
    <template id="post-template" type="text/mustache">
        {{#posts}}
        <div class="post">
            <h3>{{title}}</h3>
            <p>{{excerpt}}</p>
        </div>
        {{/posts}}
    </template>

    <button hx-get="/api/posts"
            hx-target="#post-list"
            mustache-template="post-template">
        Load Posts
    </button>
    <div id="post-list"></div>
</div>

Este enfoque funciona con plantillas Mustache, Handlebars o Nunjucks. Su WebAPI devuelve JSON, pero HTMX maneja el lado del cliente de renderizado. Es particularmente útil cuando tiene una API existente o necesita compartir datos con aplicaciones móviles. Para obtener más detalles sobre extensiones HTMX incluyendo plantillas del lado del cliente, consulte el artículo de acompañamiento.

Gotchas & Tips

Herramienta de depuración: Instalar la Extensión del depurador HTMX - muestra cada petición, respuesta e intercambio en tiempo real.

Tokens CSRF

Los tokens antifalsificación de ASP.NET Core necesitan un manejo especial con AJAX. HTMX.NET proporciona varias opciones limpias. HtmxAntiforgeryScriptEndpoint:

// In Program.cs
app.MapHtmxAntiforgeryScript();
<!-- In your layout head -->
<script src="@HtmxAntiforgeryScriptEndpoints.Path" defer></script>

O use el ayudante de etiqueta: <meta name="htmx-config" includeAspNetAntiforgeryToken="true" />. Véase Artículo de Khalid sobre Tokens Anti-Forgery HTMX para más detalles.

Alpine.js @ Taquigrafía en Razor

Alpines @click conflictos de taquigrafía con Razor's @ Sintaxis. Utilice el formulario explícito en su lugar:

<button x-on:click="doSomething()">Click me</button>  <!-- Instead of @click -->
<div x-bind:class="isOpen ? 'block' : 'hidden'"></div>  <!-- Instead of :class -->

O escapar con @@click, pero la sintaxis explícita es más clara.

Gestión del historial

Controlar las entradas del historial: Por defecto, HTMX empuja cada solicitud al historial. Para paginación/filtros donde no desea contaminación del historial:

<paging model="@Model" hx-push-url="false">  <!-- Don't add history entries -->

O usar hx-replace-url="true" actualizar la URL sin añadir entradas.

El problema Phantom Parcial: Navegador hacia atrás / hacia adelante muestra sólo un fragmento parcial sin diseño. hx-history-elt en su contenedor de contenido:

<div class="container mx-auto" id="contentcontainer" hx-history-elt>
    @RenderBody()
</div>

Esto le dice a HTMX qué elemento fotografiar, preservando el diseño circundante al restaurar la historia.

Caché CDN (Cloudflare, etc.)

Síntoma: HTMX funciona localmente, pero se rompe detrás de un CDN - el contenido incorrecto se cachea.

Causa raíz: Los CDN ignoran el HX-Request encabezado. Su servidor devuelve contenido diferente basado en este encabezado, pero la CDN los guarda en caché de forma idéntica.

ASP.NET Core fix: Uso VaryByHeaderNames:

[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" })]

CDN fix: Configurar reglas de caché para incluir HX-Request para Cloudflare: Dashboard → Caching → Reglas de caché → Clave de caché personalizada → Encabezados → Incluir HX-Request.

El debate de hx-boost

hx-boost convierte enlaces normales en peticiones AJAX. Página de peculiaridades de HTMX algunos miembros del equipo principal recomiendan evitarlo (descartes <head> contenido, afecta la localidad de comportamiento), mientras que otros lo encuentran bien para victorias rápidas.

<div hx-boost="true" hx-target="#contentcontainer">
    <a asp-action="Show" asp-route-slug="@post.Slug">Read More</a>
</div>

Este blog lo utiliza ampliamente. Al dirigirse a contenedores específicos, explícito hx-get es más claro, pero hx-boost funciona si eres consistente. Inhabilitar selectivamente con hx-boost="false" sobre los elementos infantiles.

Conclusión

HTMX con ASP.NET Los parciales del núcleo representan un retorno a la simplicidad del lado del servidor sin sacrificar la UX moderna.

  • Interacciones dinámicas, similares a las del SPA
  • Renderización del lado del servidor (genial para SEO)
  • Caché HTTP adecuado
  • Mínimo JavaScript
  • Mejora progresiva
  • Enrutamiento seguro con HTMX.NET
  • Paginación de configuración cero con principalmentelucid.pagingtaghelper

El enfoque renderizado por el servidor ha resistido la prueba del tiempo, y con HTMX, finalmente tiene la elegante herramienta del lado del cliente que se merece. Puede construir aplicaciones web robustas y performantes utilizando patrones que han funcionado durante décadas - que sólo han estado esperando la herramienta adecuada para hacerlos brillar de nuevo.

Artículos relacionados en este blog

Artículo del compañero

Este artículo es parte de una serie de dos partes sobre HTMX con ASP.NET Core:

  1. Este artículo - Se centra en la integración de ASP.NET Core, vistas parciales, HTMX.NET y paginación
  2. Un recorrido por silbidos de extensiones HTMX - Inmersión profunda en eventos HTMX, ciclo de vida, arquitectura de extensión y extensiones personalizadas

Más artículos de HTMX

Lectura adicional

Documentación oficial:

Bibliotecas y herramientas:

Recursos comunitarios:

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.