Cliente de JavaScript SignalR en ASP.NET Core

Por Rachel Appel

La biblioteca cliente de JavaScript de ASP.NET Core SignalR permite a los desarrolladores llamar al código del centro del lado del servidor SignalR.

Instalación del paquete cliente SignalR

La biblioteca de cliente de JavaScript SignalR se entrega como un paquete npm. En las secciones siguientes se describen diferentes formas de instalar la biblioteca cliente.

Instalación con npm

Ejecute los comandos siguientes en la Consola del administrador de paquetes:

npm init -y
npm install @microsoft/signalr

npm instala el contenido del paquete en la carpeta node_modules\@microsoft\signalr\dist\browser. Crea la carpeta wwwroot/lib/signalr. Copie el archivo signalr.js en la carpeta wwwroot/lib/signalr.

Haga referencia al cliente de JavaScript SignalR en el elemento <script>. Por ejemplo:

<script src="~/lib/signalr/signalr.js"></script>

Uso de una red de entrega de contenido (CDN)

Para usar la biblioteca cliente sin el requisito previo de npm, haga referencia a una copia hospedada en CDN de la biblioteca cliente. Por ejemplo:

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>

La biblioteca cliente está disponible en las siguientes redes CDN:

Instalación con LibMan

LibMan se puede usar para instalar archivos de biblioteca cliente específicos desde la biblioteca cliente hospedada en CDN. Por ejemplo, agregue solo el archivo JavaScript minimizado al proyecto. Para más detalles sobre ese método, consulte Agregue la biblioteca cliente SignalR.

Conexión a un centro

El código siguiente crea e inicia una conexión. El nombre del centro no distingue mayúsculas de minúsculas:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

// Start the connection.
start();

Conexiones entre orígenes (CORS)

Normalmente, los exploradores cargan conexiones desde el mismo dominio que la página solicitada. Sin embargo, hay ocasiones en las que se requiere una conexión a otro dominio.

Al realizar solicitudes entre dominios, el código de cliente debe usar una dirección URL absoluta en lugar de una dirección URL relativa. Para las solicitudes entre dominios, cambie .withUrl("/chathub") a .withUrl("https://{App domain name}/chathub").

Para evitar que un sitio malintencionado lea datos confidenciales de otro sitio, las conexiones entre orígenes están deshabilitadas de forma predeterminada. Para permitir una solicitud entre orígenes, habilite CORS:

using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        builder =>
        {
            builder.WithOrigins("https://example.com")
                .AllowAnyHeader()
                .WithMethods("GET", "POST")
                .AllowCredentials();
        });
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

// UseCors must be called before MapHub.
app.UseCors();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Se debe llamar a UseCors antes de llamar a MapHub.

Llamada a los métodos del centro desde el cliente

Los clientes de JavaScript llaman a métodos públicos en centros a través del método invoke de HubConnection. El método invoke acepta:

  • Nombre del método de concentrador.
  • Cualquier argumento definido en el método concentrador.

En el código resaltado siguiente, el nombre del método en el centro es SendMessage. Los argumentos segundo y tercero que se pasan a invoke se asignan a los argumentos user y message del método del centro:

try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

La llamada a los métodos del centro desde un cliente solo es compatible cuando se usa el Servicio de Azure en modo SignalRpredeterminado. Para obtener más información, consulta Preguntas más frecuentes (repositorio de GitHub de azure-signalr).

El método invoke devuelve un objeto Promise de JavaScript. Promise se resuelve con el valor devuelto (si existe) cuando el método del servidor devuelve. Si el método del servidor produce un error, Promise se rechaza con el mensaje de error. Use async y await o los métodos then y catch de Promise para controlar estos casos.

Los clientes de JavaScript también pueden llamar a métodos públicos en centros a través del método send de HubConnection. A diferencia del método invoke, el método send no espera una respuesta del servidor. El método send devuelve un objeto Promise de JavaScript. El Promise se resuelve cuando el mensaje ha sido enviado al servidor. Si se produce un error al enviar el mensaje, Promise se rechaza con el mensaje de error. Use async y await o los métodos then y catch de Promise para controlar estos casos.

El uso de send no espera hasta que el servidor haya recibido el mensaje. Por lo tanto, no es posible devolver datos o errores del servidor.

Llamada a métodos de cliente desde el centro

Para recibir mensajes desde el centro, defina un método mediante el método on del HubConnection.

  • Nombre del método cliente de JavaScript.
  • Argumentos que el centro pasa al método .

En el ejemplo siguiente, el nombre del método es ReceiveMessage. Los nombres de argumento son user y message:

connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});

El código anterior de connection.on se ejecuta cuando el código del lado servidor lo llama mediante el método SendAsync:

using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

SignalR determina a qué método cliente se debe llamar haciendo coincidir el nombre del método y los argumentos definidos en SendAsync y connection.on.

Un procedimiento recomendado es llamar al método start en HubConnection después de on. De este modo, se garantiza que los controladores se registran antes de recibir los mensajes.

Registro y control de errores

Use console.error para mostrar errores en la consola del navegador cuando el cliente no pueda conectarse o enviar un mensaje:

try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

Configure el seguimiento de registros del lado cliente pasando un registrador y un tipo de evento para registrar cuándo se realiza la conexión. Los mensajes se registran con el nivel de registro especificado y versiones posteriores. Los niveles de registro disponibles son los siguientes:

  • signalR.LogLevel.Error: mensajes de error. Solo registra mensajes de Error.
  • signalR.LogLevel.Warning: mensajes de advertencia sobre posibles errores. Registra mensajes de Warning y Error.
  • signalR.LogLevel.Information: mensajes de estado sin errores. Registra mensajes de Information, Warning y Error.
  • signalR.LogLevel.Trace: mensajes de seguimiento. Registra todo, incluidos los datos transportados entre el centro y el cliente.

Use el método configureLogging en HubConnectionBuilder para configurar el nivel de registro. Los mensajes se registran en la consola del explorador:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

Volver a conectar clientes

Volver a conectarse automáticamente

El cliente JavaScript para SignalR puede configurarse para reconectarse automáticamente usando el método WithAutomaticReconnect en HubConnectionBuilder. No se volverá a conectar automáticamente de forma predeterminada.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();

Sin ningún parámetro, WithAutomaticReconnect configura el cliente para que espere 0, 2, 10 y 30 segundos respectivamente antes de intentar cada intento de reconexión. Después de cuatro intentos fallidos, deja de intentar volver a conectarse.

Antes de iniciar cualquier intento de reconexión, el HubConnection:

  • Realiza la transición al estado HubConnectionState.Reconnecting y desencadena sus devoluciones de llamada de onreconnecting.
  • No pasa al estado Disconnected y desencadena sus devoluciones de llamada de onclose como un HubConnection sin reconexión automática configurada.

El enfoque de reconexión proporciona una oportunidad para:

  • Advertir a los usuarios de que se ha perdido la conexión.
  • Deshabilitar elementos de la interfaz de usuario.
connection.onreconnecting(error => {
    console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageList").appendChild(li);
});

Si el cliente se reconecta con éxito en sus cuatro primeros intentos, el HubConnection vuelve al estado Connected y desencadena sus devoluciones de llamada de onreconnected. Esto proporciona la oportunidad de informar a los usuarios de la conexión que se ha restablecido.

Dado que la conexión parece totalmente nueva para el servidor, se proporciona un nuevo connectionId a la devolución de llamada de onreconnected.

El parámetro connectionId de la devolución de llamada onreconnected es indefinido si el HubConnection está configurado para omitir la negociación.

connection.onreconnected(connectionId => {
    console.assert(connection.state === signalR.HubConnectionState.Connected);

    document.getElementById("messageInput").disabled = false;

    const li = document.createElement("li");
    li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
    document.getElementById("messageList").appendChild(li);
});

withAutomaticReconnect no configurará el HubConnection para reintentar los fallos de arranque iniciales, por lo que los fallos de arranque deben controlarse manualmente:

async function start() {
    try {
        await connection.start();
        console.assert(connection.state === signalR.HubConnectionState.Connected);
        console.log("SignalR Connected.");
    } catch (err) {
        console.assert(connection.state === signalR.HubConnectionState.Disconnected);
        console.log(err);
        setTimeout(() => start(), 5000);
    }
};

Si el cliente no se reconecta con éxito en sus cuatro primeros intentos, el HubConnection pasa al estado Disconnected y desencadena sus devoluciones de llamada onclose. Esto proporciona una oportunidad para notificar a los usuarios:

  • La conexión se ha perdido definitivamente.
  • Pruebe a actualizar la página:
connection.onclose(error => {
    console.assert(connection.state === signalR.HubConnectionState.Disconnected);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
    document.getElementById("messageList").appendChild(li);
});

Para configurar un número personalizado de intentos de reconexión antes de desconectar o cambiar el tiempo de reconexión, withAutomaticReconnect acepta una matriz de números que representa el retraso en milisegundos para esperar antes de iniciar cada intento de reconexión.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect([0, 0, 10000])
    .build();

    // .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior

El ejemplo anterior configura el HubConnection para que empiece a intentar reconectarse inmediatamente después de que se pierda la conexión. La configuración predeterminada también espera cero segundos para intentar volver a conectarse.

Si se produce un error en el primer intento de reconexión, el segundo intento de reconexión también se inicia inmediatamente en lugar de esperar 2 segundos con la configuración predeterminada.

Si se produce un error en el segundo intento de reconexión, el tercer intento de reconexión se inicia en 10 segundos, que es el mismo que la configuración predeterminada.

El tiempo de reconexión configurado difiere del comportamiento predeterminado al detenerse tras el fallo del tercer intento de reconexión en lugar de intentar un nuevo intento de reconexión en otros 30 segundos.

Para un mayor control sobre el tiempo y el número de intentos de reconexión automática, withAutomaticReconnect acepta un objeto que implemente la interfaz IRetryPolicy, que tiene un único método llamado nextRetryDelayInMilliseconds.

nextRetryDelayInMilliseconds toma un único argumento con el tipo RetryContext. El RetryContext tiene tres propiedades: previousRetryCount, elapsedMilliseconds y retryReason que son number, number y Error respectivamente. Antes del primer intento de reconexión, tanto previousRetryCount como elapsedMilliseconds serán cero, y retryReason será el Error que provocó la pérdida de conexión. Después de cada intento de reconexión fallido, previousRetryCount se incrementará en uno, elapsedMilliseconds se actualizará para reflejar la cantidad de tiempo empleado en la reconexión hasta el momento en milisegundos, y retryReason será el Error que causó el último intento de reconexión fallido.

nextRetryDelayInMilliseconds debe devolver un número que represente el número de milisegundos que hay que esperar antes del siguiente intento de reconexión o null si HubConnection debe dejar de reconectarse.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            if (retryContext.elapsedMilliseconds < 60000) {
                // If we've been reconnecting for less than 60 seconds so far,
                // wait between 0 and 10 seconds before the next reconnect attempt.
                return Math.random() * 10000;
            } else {
                // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
                return null;
            }
        }
    })
    .build();

Como alternativa, se puede escribir código que vuelva a conectar al cliente manualmente, como se demuestra en la siguiente sección.

Volver a conectarse manualmente

En el código siguiente se muestra un enfoque típico de reconexión manual:

  1. Se crea una función (en este caso, la función start) para iniciar la conexión.
  2. Llame a la función start en el controlador de eventos onclose de la conexión.
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

Las implementaciones de producción suelen usar un retroceso exponencial o reintentar un número especificado de veces.

Pestaña de suspensión del explorador

Algunos exploradores tienen una característica de bloqueo de pestañas o de suspensión para reducir el uso de recursos del equipo para pestañas inactivas. Esto puede provocar el cierre de conexiones de SignalR y puede dar lugar a una experiencia de usuario no deseada. Los exploradores usan heurística para averiguar si se debe poner en suspensión una pestaña, como:

  • Reproducir audio
  • Mantener un bloqueo web
  • Mantener un bloqueo IndexedDB
  • Estar conectado a un dispositivo USB
  • Capturar vídeo o audio
  • Crear un reflejo
  • Capturar una ventana o pantalla

La heurística del explorador puede cambiar con el tiempo y puede diferir entre exploradores. Compruebe la matriz de compatibilidad y descubra qué método funciona mejor para sus escenarios.

Para evitar la suspensión de una aplicación, la aplicación debería estar desencadenando una de las heurísticas que usa el navegador.

En el ejemplo de código siguiente se muestra cómo usar un bloqueo web para mantener activa una pestaña y evitar un cierre de conexión inesperado.

var lockResolver;
if (navigator && navigator.locks && navigator.locks.request) {
    const promise = new Promise((res) => {
        lockResolver = res;
    });

    navigator.locks.request('unique_lock_name', { mode: "shared" }, () => {
        return promise;
    });
}

Para el ejemplo de código anterior:

  • Los bloqueos web son experimentales. La comprobación condicional confirma que el explorador admite bloqueos web.
  • El solucionador de promesas, lockResolver, se almacena para que el bloqueo se pueda liberar cuando sea aceptable que la pestaña se suspenda.
  • Al cerrar la conexión, el bloqueo se libera mediante una llamada a lockResolver(). Cuando se libera el bloqueo, la pestaña puede suspenderse.

Recursos adicionales

Por Rachel Appel

La biblioteca cliente de JavaScript de ASP.NET Core SignalR permite a los desarrolladores llamar al código del centro del lado del servidor.

Vea o descargue el código de ejemplo (cómo descargarlo)

Instalación del paquete cliente SignalR

La biblioteca de cliente de JavaScript SignalR se entrega como un paquete npm. En las secciones siguientes se describen diferentes formas de instalar la biblioteca cliente.

Instalación con npm

Para Visual Studio, ejecute los siguientes comandos desde la consola del administrador de paquetes mientras se encuentra en la carpeta raíz. Para Visual Studio Code, ejecute los siguientes comandos desde el Terminal integrado.

npm init -y
npm install @microsoft/signalr

npm instala el contenido del paquete en la carpeta node_modules\@microsoft\signalr\dist\browser. Crea una carpeta denominada signalr en la carpeta wwwroot\lib. Copia el archivo signalr.js en la carpeta wwwroot\lib\signalr.

Haga referencia al cliente de JavaScript SignalR en el elemento <script>. Por ejemplo:

<script src="~/lib/signalr/signalr.js"></script>

Uso de una red de entrega de contenido (CDN)

Para usar la biblioteca cliente sin el requisito previo de npm, haga referencia a una copia hospedada en CDN de la biblioteca cliente. Por ejemplo:

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.js"></script>

La biblioteca cliente está disponible en las siguientes redes CDN:

Instalación con LibMan

LibMan se puede usar para instalar archivos de biblioteca cliente específicos desde la biblioteca cliente hospedada en CDN. Por ejemplo, agregue solo el archivo JavaScript minimizado al proyecto. Para más detalles sobre ese método, consulte Agregue la biblioteca cliente SignalR.

Conexión a un centro

El código siguiente crea e inicia una conexión. El nombre del centro no distingue mayúsculas de minúsculas:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

// Start the connection.
start();

Conexiones entre orígenes

Normalmente, los exploradores cargan conexiones desde el mismo dominio que la página solicitada. Sin embargo, hay ocasiones en las que se requiere una conexión a otro dominio.

Importante

El código de cliente debe usar una dirección URL absoluta en lugar de una dirección URL relativa. Cambio de .withUrl("/chathub") a .withUrl("https://myappurl/chathub").

Para evitar que un sitio malintencionado lea datos confidenciales de otro sitio, las conexiones entre orígenes están deshabilitadas de forma predeterminada. Para permitir una solicitud de origen cruzado, habilítela en la clase Startup:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;

namespace SignalRChat
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddSignalR();

            services.AddCors(options =>
            {
                options.AddDefaultPolicy(builder =>
                {
                    builder.WithOrigins("https://example.com")
                        .AllowCredentials();
                });
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseCors();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapHub<ChatHub>("/chathub");
            });
        }
    }
}

Llamada a los métodos del centro desde el cliente

Los clientes de JavaScript llaman a métodos públicos en centros a través del método invoke de HubConnection. El método invoke acepta:

  • Nombre del método de concentrador.
  • Cualquier argumento definido en el método concentrador.

En el siguiente ejemplo, el nombre del método en el centro es SendMessage. Los argumentos segundo y tercero que se pasan a invoke se asignan a los argumentos user y message del método del centro:

try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

Nota:

La llamada a los métodos del concentrador desde un cliente solo es compatible cuando se usa el Servicio de Azure SignalR en modo Predeterminado. Para obtener más información, consulta Preguntas más frecuentes (repositorio de GitHub de azure-signalr).

El método invoke devuelve un objeto Promise de JavaScript. Promise se resuelve con el valor devuelto (si existe) cuando el método del servidor devuelve. Si el método del servidor produce un error, Promise se rechaza con el mensaje de error. Use async y await o los métodos then y catch de Promise para controlar estos casos.

Los clientes de JavaScript también pueden llamar a métodos públicos en centros a través del método send de HubConnection. A diferencia del método invoke, el método send no espera una respuesta del servidor. El método send devuelve un objeto Promise de JavaScript. El Promise se resuelve cuando el mensaje ha sido enviado al servidor. Si se produce un error al enviar el mensaje, Promise se rechaza con el mensaje de error. Use async y await o los métodos then y catch de Promise para controlar estos casos.

Nota

El uso de send no espera a que el servidor haya recibido el mensaje. Por lo tanto, no es posible devolver datos o errores del servidor.

Llamada a métodos de cliente desde el centro

Para recibir mensajes desde el centro, defina un método mediante el método on del HubConnection.

  • Nombre del método cliente de JavaScript.
  • Argumentos que el centro pasa al método .

En el ejemplo siguiente, el nombre del método es ReceiveMessage. Los nombres de argumento son user y message:

connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});

El código anterior de connection.on se ejecuta cuando el código del lado servidor lo llama mediante el método SendAsync:

public async Task SendMessage(string user, string message)
{
    await Clients.All.SendAsync("ReceiveMessage", user, message);
}

SignalR determina a qué método cliente se debe llamar haciendo coincidir el nombre del método y los argumentos definidos en SendAsync y connection.on.

Nota

Un procedimiento recomendado es llamar al método start en HubConnection después de on. Al hacerlo, se asegura de que los controladores se registren antes de que se reciban mensajes.

Registro y control de errores

Use try y catch con async y await o el método catch de Promise para controlar los errores del lado del cliente. Use console.error para generar errores en la consola del explorador:

try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

Configure el seguimiento de registros del lado cliente pasando un registrador y un tipo de evento para registrar cuándo se realiza la conexión. Los mensajes se registran con el nivel de registro especificado y versiones posteriores. Los niveles de registro disponibles son los siguientes:

  • signalR.LogLevel.Error: mensajes de error. Solo registra mensajes de Error.
  • signalR.LogLevel.Warning: mensajes de advertencia sobre posibles errores. Registra mensajes de Warning y Error.
  • signalR.LogLevel.Information: mensajes de estado sin errores. Registra mensajes de Information, Warning y Error.
  • signalR.LogLevel.Trace: mensajes de seguimiento. Registra todo, incluidos los datos transportados entre el centro y el cliente.

Use el método configureLogging en HubConnectionBuilder para configurar el nivel de registro. Los mensajes se registran en la consola del explorador:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

Volver a conectar clientes

Volver a conectarse automáticamente

El cliente de JavaScript para SignalR puede configurarse para reconectarse automáticamente usando el método withAutomaticReconnect en HubConnectionBuilder. No se volverá a conectar automáticamente de forma predeterminada.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();

Sin ningún parámetro, withAutomaticReconnect() configura el cliente para que espere 0, 2, 10 y 30 segundos respectivamente antes de intentar cada intento de reconexión, deteniéndose tras cuatro intentos fallidos.

Antes de iniciar cualquier intento de reconexión, HubConnection pasará al estado HubConnectionState.Reconnecting y activará sus devoluciones de llamada de onreconnecting en lugar de pasar al estado Disconnected y desencadenar sus devoluciones de llamada de onclose como un HubConnection sin reconexión automática configurada. Esto proporciona una oportunidad para advertir a los usuarios de que se ha perdido la conexión y deshabilitar los elementos de la interfaz de usuario.

connection.onreconnecting(error => {
    console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageList").appendChild(li);
});

Si el cliente se reconecta con éxito en sus cuatro primeros intentos, el HubConnection vuelve al estado Connected y desencadena sus devoluciones de llamada de onreconnected. Esto proporciona la oportunidad de informar a los usuarios de la conexión que se ha restablecido.

Dado que la conexión parece totalmente nueva para el servidor, se proporciona un nuevo connectionId a la devolución de llamada de onreconnected.

Advertencia

El parámetro connectionId de la devolución de llamada onreconnected será indefinido si HubConnection se configuró para omitir la negociación.

connection.onreconnected(connectionId => {
    console.assert(connection.state === signalR.HubConnectionState.Connected);

    document.getElementById("messageInput").disabled = false;

    const li = document.createElement("li");
    li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
    document.getElementById("messageList").appendChild(li);
});

withAutomaticReconnect() no configurará HubConnection para reintentar los fallos de arranque iniciales, por lo que los fallos de arranque deben controlarse manualmente:

async function start() {
    try {
        await connection.start();
        console.assert(connection.state === signalR.HubConnectionState.Connected);
        console.log("SignalR Connected.");
    } catch (err) {
        console.assert(connection.state === signalR.HubConnectionState.Disconnected);
        console.log(err);
        setTimeout(() => start(), 5000);
    }
};

Si el cliente no se reconecta con éxito en sus cuatro primeros intentos, HubConnection pasa al estado Disconnected y desencadena sus devoluciones de llamada onclose. Esto proporciona la oportunidad de informar a los usuarios de que la conexión se ha perdido permanentemente y recomendar la actualización de la página:

connection.onclose(error => {
    console.assert(connection.state === signalR.HubConnectionState.Disconnected);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
    document.getElementById("messageList").appendChild(li);
});

Para configurar un número personalizado de intentos de reconexión antes de desconectar o cambiar el tiempo de reconexión, withAutomaticReconnect acepta una matriz de números que representa el retraso en milisegundos para esperar antes de iniciar cada intento de reconexión.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect([0, 0, 10000])
    .build();

    // .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior

El ejemplo anterior configura el HubConnection para que empiece a intentar reconectarse inmediatamente después de que se pierda la conexión. Esto también es cierto para la configuración predeterminada.

Si se produce un error en el primer intento de reconexión, el segundo intento de reconexión también se iniciará inmediatamente en lugar de esperar 2 segundos como lo haría en la configuración predeterminada.

Si se produce un error en el segundo intento de reconexión, el tercer intento de reconexión se iniciará en 10 segundos, como la configuración predeterminada.

Después, el comportamiento personalizado vuelve a desviarse del comportamiento predeterminado al detenerse tras el fracaso del tercer intento de reconexión en lugar de intentar una reconexión más en otros 30 segundos como haría en la configuración predeterminada.

Si quiere aún más control sobre el tiempo y el número de intentos de reconexión automática, withAutomaticReconnect acepta un objeto que implemente la interfaz IRetryPolicy, que tiene un único método llamado nextRetryDelayInMilliseconds.

nextRetryDelayInMilliseconds toma un único argumento con el tipo RetryContext. El RetryContext tiene tres propiedades: previousRetryCount, elapsedMilliseconds y retryReason que son number, number y Error respectivamente. Antes del primer intento de reconexión, tanto previousRetryCount como elapsedMilliseconds serán cero, y retryReason será el Error que provocó la pérdida de conexión. Después de cada intento de reconexión fallido, previousRetryCount se incrementará en uno, elapsedMilliseconds se actualizará para reflejar la cantidad de tiempo empleado en la reconexión hasta el momento en milisegundos, y retryReason será el Error que causó el último intento de reconexión fallido.

nextRetryDelayInMilliseconds debe devolver un número que represente el número de milisegundos que hay que esperar antes del siguiente intento de reconexión o null si HubConnection debe dejar de reconectarse.

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            if (retryContext.elapsedMilliseconds < 60000) {
                // If we've been reconnecting for less than 60 seconds so far,
                // wait between 0 and 10 seconds before the next reconnect attempt.
                return Math.random() * 10000;
            } else {
                // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
                return null;
            }
        }
    })
    .build();

Como alternativa, puede escribir código que vuelva a conectar el cliente manualmente, como se muestra en Reconexión manual.

Volver a conectarse manualmente

En el código siguiente se muestra un enfoque típico de reconexión manual:

  1. Se crea una función (en este caso, la función start) para iniciar la conexión.
  2. Llame a la función start en el controlador de eventos onclose de la conexión.
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

Las implementaciones de producción suelen usar un retroceso exponencial o reintentar un número especificado de veces.

Pestaña de suspensión del explorador

Algunos exploradores tienen una característica de bloqueo de pestañas o de suspensión para reducir el uso de recursos del equipo para pestañas inactivas. Esto puede provocar el cierre de conexiones de SignalR y puede dar lugar a una experiencia de usuario no deseada. Los exploradores usan heurística para averiguar si se debe poner en suspensión una pestaña, como:

  • Reproducir audio
  • Mantener un bloqueo web
  • Mantener un bloqueo IndexedDB
  • Estar conectado a un dispositivo USB
  • Capturar vídeo o audio
  • Crear un reflejo
  • Capturar una ventana o pantalla

Nota

Esta heurística puede cambiar con el tiempo o diferir entre navegadores. Compruebe la matriz de compatibilidad y descubra qué método funciona mejor para sus escenarios.

Para evitar la suspensión de una aplicación, la aplicación debería estar desencadenando una de las heurísticas que usa el navegador.

En el ejemplo de código siguiente se muestra cómo usar un bloqueo web para mantener activa una pestaña y evitar un cierre de conexión inesperado.

var lockResolver;
if (navigator && navigator.locks && navigator.locks.request) {
    const promise = new Promise((res) => {
        lockResolver = res;
    });

    navigator.locks.request('unique_lock_name', { mode: "shared" }, () => {
        return promise;
    });
}

Para el ejemplo de código anterior:

  • Los bloqueos web son experimentales. La comprobación condicional confirma que el explorador admite bloqueos web.
  • El solucionador de promesas (lockResolver) se almacena para que el bloqueo se pueda liberar cuando sea aceptable que la pestaña se suspenda.
  • Al cerrar la conexión, el bloqueo se libera mediante una llamada a lockResolver(). Cuando se libera el bloqueo, la pestaña puede suspenderse.

Recursos adicionales