ASP.NET Core SignalR клиент JavaScript

Клиентская библиотека JavaScript для ASP.NET Core SignalR позволяет разработчикам вызывать код концентратора на стороне SignalR сервера. В этой статье описывается, как использовать API для подключения к SignalR концентратору и вызова концентратора JavaScript и клиентских методов.

Просмотреть или скачать образец кода (описание загрузки)

Установка клиентского SignalR пакета

Клиентская SignalR библиотека JavaScript поставляется в виде пакета npm . Существует несколько способов установки клиентской библиотеки:

  • Выполните команды npm в консоли Visual Studio диспетчер пакетов.
  • Выполните команды npm в интегрированном терминале в Visual Studio Code.
  • Ссылка на копию клиентской библиотеки, размещенной в сети доставки содержимого (CDN).
  • Используйте LibMan и установите определенные файлы клиентской библиотеки из клиентской библиотеки, размещенной в CDN.

Установка с помощью npm

Выполните следующие команды npm в окне консоли диспетчер пакетов:

npm init -y
npm install @microsoft/signalr

npm устанавливает содержимое пакета в папку node_modules\@microsoft\signalr\dist\browser .

Завершите настройку:

  1. Создайте папку wwwroot/lib/signalr .

  2. Скопируйте файлsignalr.js в папку wwwroot/lib/signalr .

Теперь вы можете ссылаться на установленный SignalR клиент JavaScript в элементе <script> . Рассмотрим пример.

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

Использование CDN

Чтобы использовать клиентную библиотеку без предварительных требований npm, наведите ссылку на размещенную в CDN копию клиентской библиотеки. Рассмотрим пример.

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

В примере кода указывается версия 6.0.1. Чтобы получить последнюю версию клиентской библиотеки, выберите один из следующих CDN:

Установка с помощью LibMan

Другой подход — использовать LibMan и устанавливать только определенные файлы клиентской библиотеки из клиентской библиотеки CDN. Например, в проект можно добавить только минифицированный файл JavaScript.

Дополнительные сведения об этом подходе см. в разделе "Добавление клиентской SignalR библиотеки".

Подключение к концентратору

Следующий код создает и запускает подключение. Имя хаба нечувствительно к регистру:

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();

Подключения между источниками (CORS)

Как правило, браузеры загружают подключения из того же домена, что и запрошенная страница. Однако иногда требуется подключение к другому домену.

Для запросов между доменами код клиента должен использовать абсолютный URL-адрес, а не относительный URL-адрес. Если вы используете междоменные запросы, измените его .withUrl("/chathub") на .withUrl("https://{App domain name}/chathub").

Чтобы предотвратить чтение конфиденциальных данных с другого сайта вредоносным сайтом, по умолчанию подключения между источниками отключены. Чтобы разрешить междоменный запрос, включите 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();

Метод UseCors необходимо вызвать перед вызовом метода MapHub.

Вызов методов концентратора из клиента

Клиенты JavaScript вызывают открытые методы в концентраторах через метод invoke объекта HubConnection. Метод invoke принимает:

  • Имя метода концентратора.
  • Все аргументы, определённые в методе концентратора.

В следующем выделенном коде имя метода в концентраторе равно SendMessage. Второй и третий аргументы, переданные в invoke, соответствуют аргументам user и message метода концентратора:

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

Вызов методов концентратора из клиента поддерживается только при использовании службы Azure SignalR Service в режиме по умолчанию. Для получения дополнительной информации см. часто задаваемые вопросы (репозиторий GitHub azure-signalr).

Метод invoke возвращает объект JavaScript Promise . Объекту Promise присваивается значение, возвращаемое методом (если оно есть), когда серверный метод завершает выполнение. Если метод на сервере выдает ошибку, Promise объект отклоняется с сообщением об ошибке. Для обработки этих случаев используйте async и await или методы then и catch объекта Promise.

Клиенты JavaScript также могут вызывать общедоступные методы в центрах с помощью метода отправкиHubConnection. invoke В отличие от метода, send метод не ожидает ответа от сервера. Метод send возвращает объект JavaScript Promise . Объект Promise определяется при отправке сообщения на сервер. Если при отправке сообщения возникает ошибка, Promise объект отклоняется с сообщением об ошибке. Для обработки этих случаев используйте async и await или методы then и catch объекта Promise.

При использовании sendне происходит ожидания того, что сервер получит сообщение, поэтому невозможно получить от сервера данные или сообщения об ошибках.

Вызов методов клиента из хаба

Чтобы получать сообщения от концентратора, определите метод с помощью метода on объекта HubConnection. Метод on принимает:

  • Имя клиентского метода JavaScript.
  • Аргументы, которые концентратор передает методу.

В следующем примере имя метода — ReceiveMessage. Имена аргументов:usermessage

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

Предыдущий код в connection.on выполняется, когда код на стороне сервера вызывает его с помощью метода 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 определяет, какой метод клиента следует вызывать путем сопоставления имени метода и аргументов, определенных в SendAsync и connection.on.

Рекомендуется вызывать метод start у HubConnection после on. Этот подход гарантирует, что обработчики регистрируются до получения сообщений.

Обработка ошибок и ведение журнала

Используется console.error для вывода ошибок в консоль браузера, когда клиент не может подключиться или отправить сообщение:

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

Настройте трассировку логов на стороне клиента, передав логгер и тип события, которое нужно регистрировать при установлении подключения. Сообщения записываются в журнал с указанным уровнем ведения журнала и более высоким. Доступные уровни журнала приведены следующим образом:

Уровень логирования Зарегистрированные данные Description
signalR.LogLevel.Error Сообщения об ошибках Error Регистрирует только сообщения.
signalR.LogLevel.Warning Предупреждающие сообщения о потенциальных ошибках Регистрирует сообщения Warning и Error.
signalR.LogLevel.Information Сообщения о состоянии без ошибок Регистрирует сообщения Information, Warning и Error.
signalR.LogLevel.Trace Отслеживание сообщений Регистрирует все данные, включая данные, транспортируемые между концентратором и клиентом.

Используйте метод configureLogging в HubConnectionBuilder, чтобы настроить уровень журнала. Сообщения записываются в консоль браузера:

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

Повторное подключение клиентов

Чтобы повторно подключиться к клиентам, можно настроить автоматическое повторное подключение или настроить подключение вручную.

Автоматическое повторное подключение

Клиент SignalR JavaScript можно настроить для автоматического повторного подключения с помощью метода WithAutomaticReconnect в HubConnectionBuilder. По умолчанию он не выполняет автоматическое повторное подключение.

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

Без параметров WithAutomaticReconnect настраивает клиент ждать 0, 2, 10 и 30 секунд соответственно, прежде чем пытаться повторно подключиться. После четырех неудачных попыток он останавливает попытку повторного подключения.

Перед началом любого повторного подключения: HubConnection

  • Переходит в состояние HubConnectionState.Reconnecting и запускает обратные onreconnecting вызовы.
  • Не переходит в состояние Disconnected и не вызывает свои обратные вызовы onclose, как HubConnection без настроенного автоматического переподключения.

Подход повторного подключения предоставляет возможность предупреждать пользователей о том, что подключение потеряно и отключает элементы пользовательского интерфейса.

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);
});

Если клиент успешно переподключается в течение первых четырех попыток, HubConnection переходит обратно в состояние Connected и вызывает свои обратные вызовы onreconnected. Этот подход позволяет сообщить пользователям о том, что подключение теперь будет восстановлено.

Так как подключение выглядит для сервера совершенно новым, в обратный вызов onreconnected передаётся новый connectionId.

Параметр onreconnectedобратного вызова connectionId не определен, если HubConnection настроено пропустить согласование.

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 не настраивает HubConnection на повторные попытки при сбоях первоначального запуска, поэтому такие сбои необходимо обрабатывать вручную:

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);
    }
};

Если клиенту не удается успешно переподключиться в течение первых четырёх попыток, HubConnection переходит в состояние Disconnected и вызывает свои обработчики обратного вызова onclose. Этот подход предоставляет возможность сообщить пользователям о том, что подключение окончательно потеряно, и попытаться обновить страницу:

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);
});

Чтобы настроить настраиваемое количество попыток повторного подключения перед отключением или изменением времени повторного подключения, withAutomaticReconnect принимает массив чисел, представляющий задержку в миллисекундах, чтобы ждать перед началом каждой попытки повторного подключения.

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

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

В предыдущем примере HubConnection настраивается так, чтобы он начинал пытаться выполнять повторные подключения сразу после потери соединения. Конфигурация по умолчанию также ожидает от нуля (0) секунд, чтобы попытаться повторно подключиться.

  • Если первая попытка повторного подключения завершается ошибкой, вторая попытка повторного подключения также запускается немедленно, а не ожидает 2 секунд, как определено в конфигурации по умолчанию.

  • Если вторая попытка повторного подключения завершается сбоем, третья попытка повторного подключения начинается в 10 секунд, то это то же поведение, определенное в конфигурации по умолчанию.

  • Настроенные сроки повторного подключения отличаются от поведения по умолчанию путем остановки после третьей попытки повторного подключения. В конфигурации по умолчанию выполняется еще одна попытка повторного подключения через 30 секунд.

Для получения большего контроля над временем и количеством попыток withAutomaticReconnect автоматического повторного подключения принимает объект, реализующий IRetryPolicy интерфейс, имеющий один метод с именем nextRetryDelayInMilliseconds. nextRetryDelayInMilliseconds принимает один аргумент с типом RetryContext. Имеет RetryContext три свойства: previousRetryCount (тип), number (типelapsedMillisecondsnumber) и retryReason (типError).

  • Перед первой попыткой повторного подключения previousRetryCount и elapsedMilliseconds оба равны нулю (0), а retryReason — это ошибка, вызвавшая потерю соединения.

  • После каждой неудачной попытки повторного подключения значение previousRetryCount увеличивается на единицу, elapsedMilliseconds обновляется, чтобы отражать время, уже затраченное на повторное подключение, в миллисекундах, а retryReason содержит ошибку, из-за которой последняя попытка повторного подключения завершилась неудачей.

nextRetryDelayInMilliseconds должен возвращать либо число, обозначающее количество миллисекунд, которое нужно подождать перед следующей попыткой переподключения, либо null, если HubConnection должен прекратить попытки переподключения.

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();

Кроме того, можно написать код для повторного подключения клиента вручную, как показано в следующем разделе.

Повторное подключение вручную

Следующий код демонстрирует типичный подход повторного подключения вручную:

  1. Чтобы запустить подключение, создайте функцию. В этом случае это start функция.

  2. Вызовите функцию start в обработчике событий подключения onclose .

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

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

В промышленных реализациях обычно используется экспоненциальная задержка между повторами либо повторная попытка выполняется заданное число раз.

Вкладка "Спящий браузер"

Некоторые браузеры имеют функцию замораживания вкладок или спящей вкладки , которая снижает использование ресурсов компьютера для неактивных вкладок. Однако эта функция может привести к закрытию SignalR соединений и ухудшению пользовательского опыта.

Браузеры реализуют эвристики, чтобы определить, следует ли поместить вкладку в спящий режим, например:

  • Воспроизведение звука
  • Удержание веб-блокировки
  • IndexedDB Хранение блокировки
  • Подключение к USB-устройству
  • Запись видео или звука
  • Зеркальное отображение
  • Запись окна или отображения

Эвристика может меняться со временем, а также различаться в разных браузерах. Проверьте матрицу поддержки и узнайте, какой метод лучше всего подходит для ваших сценариев.

Чтобы избежать перевода приложения в спящий режим, приложение должно задействовать одну из эвристик, используемых браузером.

В следующем примере кода показано, как использовать Web Lock, чтобы вкладка оставалась активной и избежать неожиданного разрыва соединения:

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;
    });
}

В предыдущем коде:

  • Веб-блокировки являются экспериментальными. Условный флажок подтверждает, что браузер поддерживает веб-блокировки.
  • Сопоставитель обещаний JavaScript (lockResolver) хранится, чтобы блокировка была освобождена, когда она допустима для перехода на вкладку в спящий режим.
  • При закрытии подключения блокировка освобождается путем вызова lockResolver(). Когда блокировка снимается, вкладке разрешается перейти в спящий режим.

Автор: Рэйчел Аппель (Rachel Appel)

Клиентская библиотека JavaScript для ASP.NET Core SignalR позволяет разработчикам вызывать код концентратора на стороне сервера.

Просмотреть или скачать образец кода (описание загрузки)

Установка клиентского SignalR пакета

Клиентская SignalR библиотека JavaScript поставляется в виде пакета npm . В следующих разделах описаны различные способы установки клиентской библиотеки.

Установка с помощью npm

Для Visual Studio выполните следующие команды из консоли диспетчер пакетов во время работы в корневой папке. Для Visual Studio Code выполните следующие команды из интегрированного терминала.

npm init -y
npm install @microsoft/signalr

npm устанавливает содержимое пакета в папку node_modules\@microsoft\signalr\dist\browser . Создайте новую папку с именем signalr в папке wwwroot\lib . Скопируйте файл в signalr.js папку wwwroot\lib\signalr .

Укажите клиент JavaScript SignalR в элементе <script>. Рассмотрим пример.

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

Используйте сеть доставки контента (CDN)

Чтобы использовать клиентную библиотеку без предварительных требований npm, наведите ссылку на размещенную в CDN копию клиентской библиотеки. Рассмотрим пример.

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

Клиентская библиотека доступна на следующих CDN:

Установка с помощью LibMan

LibMan можно использовать для установки определенных файлов клиентской библиотеки из клиентской библиотеки, размещенной в CDN. Например, добавьте в проект только минифицированный файл JavaScript. Дополнительные сведения об этом подходе см. в разделе "Добавление клиентской SignalR библиотеки".

Подключение к концентратору

Следующий код создает и запускает подключение. Имя концентратора является нечувствительным к регистру:

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();

Подключения между источниками

Как правило, браузеры загружают подключения из того же домена, что и запрошенная страница. Однако иногда требуется подключение к другому домену.

Important

Клиентский код должен использовать абсолютный URL-адрес вместо относительного URL-адреса. Измените .withUrl("/chathub") на .withUrl("https://myappurl/chathub").

Чтобы предотвратить чтение конфиденциальных данных с другого сайта вредоносным сайтом, по умолчанию подключения между источниками отключены. Чтобы разрешить междоменный запрос, включите это в классе 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");
            });
        }
    }
}

Вызов методов концентратора из клиента

Клиенты JavaScript вызывают открытые методы в концентраторах через метод invoke объекта HubConnection. Метод invoke принимает:

  • Имя метода концентратора.
  • Все аргументы, определённые в методе концентратора.

В следующем примере имя метода в концентраторе равно SendMessage. Второй и третий аргументы, переданные в invoke, соответствуют аргументам user и message метода концентратора:

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

Note

Вызов методов концентратора из клиента поддерживается только при использовании службы Azure SignalR в режиме по умолчанию . Для получения дополнительной информации см. часто задаваемые вопросы (репозиторий GitHub azure-signalr).

Метод invoke возвращает JavaScript Promise. Promise получает возвращаемое значение (если оно есть), когда серверный метод завершает выполнение. Если метод на сервере вызывает ошибку, Promise отклоняется с сообщением об ошибке. Используйте async и await или методы then и catch объекта Promise для обработки этих случаев.

Клиенты JavaScript также могут вызывать общедоступные методы в центрах с помощью метода отправкиHubConnection. invoke В отличие от метода, send метод не ожидает ответа от сервера. Метод send возвращает JavaScript Promise. Promise выполняется, когда сообщение было отправлено на сервер. Если при отправке сообщения возникает ошибка, Promise отклоняется с сообщением об ошибке. Используйте async и await или методы then и catch объекта Promise для обработки этих случаев.

Note

Использование send не ожидает, пока сервер не получит сообщение. Следовательно, невозможно вернуть данные или ошибки с сервера.

Вызов методов клиента из хаба

Чтобы получать сообщения из концентратора, определите метод, используя метод on объекта HubConnection.

  • Имя клиентского метода JavaScript.
  • Аргументы, которые концентратор передает методу.

В следующем примере имя метода — ReceiveMessage. Имена аргументов:usermessage

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

Приведённый выше код в connection.on выполняется, когда код на стороне сервера вызывает его с помощью метода SendAsync:

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

SignalR определяет, какой метод клиента следует вызывать путем сопоставления имени метода и аргументов, определенных в SendAsync и connection.on.

Note

Рекомендуется вызвать метод start для HubConnection после on. Это гарантирует регистрацию обработчиков перед получением сообщений.

Обработка ошибок и ведение журнала

Используйте try и catch с async и await или метод catch у Promise для обработки ошибок на стороне клиента. Используется console.error для вывода ошибок в консоли браузера:

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

Настройте трассировку логов на стороне клиента, передав логгер и тип события, которое нужно регистрировать при установлении подключения. Сообщения записываются в журнал с указанным уровнем ведения журнала и более высоким. Доступные уровни журнала приведены следующим образом:

  • signalR.LogLevel.Error: сообщения об ошибках. Error Регистрирует только сообщения.
  • signalR.LogLevel.Warning: Предупреждающие сообщения о возможных ошибках. Регистрирует сообщения Warning и Error.
  • signalR.LogLevel.Information: сообщения о статусе без ошибок. Регистрирует сообщения Information, Warning и Error.
  • signalR.LogLevel.Trace: трассировка сообщений. Регистрирует все данные, включая данные, транспортируемые между концентратором и клиентом.

Используйте метод configureLogging в HubConnectionBuilder, чтобы настроить уровень журнала. Сообщения записываются в консоль браузера:

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

Повторное подключение клиентов

Автоматическое повторное подключение

Клиент JavaScript для SignalR можно настроить на автоматическое переподключение с помощью метода withAutomaticReconnect в HubConnectionBuilder. По умолчанию он не будет автоматически повторно подключаться.

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

Без параметров withAutomaticReconnect() клиент настраивает ожидание 0, 2, 10 и 30 секунд соответственно, прежде чем пытаться выполнить каждую попытку повторного подключения, остановившись после четырех неудачных попыток.

Прежде чем начнутся какие-либо попытки повторного подключения, HubConnection перейдет в состояние HubConnectionState.Reconnecting и вызовет свои колбэки onreconnecting вместо перехода в состояние Disconnected и вызова колбэков onclose, как это происходило бы у HubConnection без настроенного автоматического переподключения. Это дает возможность предупредить пользователей о том, что подключение потеряно и отключает элементы пользовательского интерфейса.

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);
});

Если клиент успешно повторно подключается в течение первых четырех попыток, HubConnection он вернется к Connected состоянию и вызовет обратный onreconnected вызов. Это дает возможность сообщить пользователям, что подключение было восстановлено.

Поскольку для сервера это подключение выглядит совершенно новым, в обратный вызов onreconnected будет передан новый connectionId.

Warning

Параметр onreconnected обратного вызова connectionId не определен, если HubConnection настроено пропустить согласование.

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() не настраивает HubConnection на повторные попытки при сбоях первоначального запуска, поэтому такие сбои запуска необходимо обрабатывать вручную:

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);
    }
};

Если клиенту не удаётся повторно подключиться в течение первых четырёх попыток, HubConnection перейдёт в состояние Disconnected и вызовет свои обратные вызовы onclose. Это дает возможность информировать пользователей о том, что подключение было окончательно потеряно и рекомендуется обновить страницу:

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);
});

Чтобы настроить настраиваемое количество попыток повторного подключения перед отключением или изменением времени повторного подключения, withAutomaticReconnect принимает массив чисел, представляющий задержку в миллисекундах, чтобы ждать перед началом каждой попытки повторного подключения.

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

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

В предыдущем примере HubConnection настраивается так, чтобы он начинал пытаться выполнять повторные подключения сразу после потери соединения. Это также верно для конфигурации по умолчанию.

Если первая попытка повторного подключения завершается сбоем, вторая попытка повторного подключения также начнется немедленно, а не ожидает 2 секунд, как в конфигурации по умолчанию.

Если вторая попытка повторного подключения завершается ошибкой, третья попытка повторного подключения начнется через 10 секунд, которая снова похожа на конфигурацию по умолчанию.

Затем настраиваемое поведение снова расходится от поведения по умолчанию, остановив после третьей попытки повторного подключения, вместо того, чтобы попробовать еще одну попытку повторного подключения в еще 30 секунд, как это было бы в конфигурации по умолчанию.

Если требуется еще больше контроля над временем и количеством попыток автоматического повторного подключения, withAutomaticReconnect принимает объект, реализующий IRetryPolicy интерфейс, имеющий один метод с именем nextRetryDelayInMilliseconds.

nextRetryDelayInMilliseconds принимает один аргумент с типом RetryContext. У RetryContext есть три свойства: previousRetryCount, elapsedMilliseconds и retryReason, которые представляют собой соответственно number, number и Error. Перед первой попыткой повторного подключения и previousRetryCount, и elapsedMilliseconds будут равны нулю, а retryReason будет ошибкой, которая привела к потере соединения. После каждой неудачной попытки повторного подключения значение previousRetryCount будет увеличиваться на единицу, elapsedMilliseconds будет обновляться, отражая общее время, затраченное к этому моменту на повторное подключение, в миллисекундах, а в retryReason будет содержаться ошибка, из-за которой последняя попытка повторного подключения завершилась неудачно.

nextRetryDelayInMilliseconds должен возвращать либо число, обозначающее количество миллисекунд, которое нужно подождать перед следующей попыткой переподключения, либо null, если HubConnection должен прекратить попытки переподключения.

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();

Кроме того, можно написать код, который будет повторно подключать клиент вручную, как показано в ручном повторном подключении.

Повторное подключение вручную

Следующий код демонстрирует типичный подход повторного подключения вручную:

  1. Функция (в данном случае start функция) создается для запуска подключения.
  2. Вызовите функцию start в обработчике событий подключения onclose .
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

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

В промышленных реализациях обычно используется экспоненциальная задержка между повторами либо повторная попытка выполняется заданное число раз.

Вкладка "Спящий браузер"

Некоторые браузеры имеют функцию замораживания или спящего состояния, чтобы уменьшить использование ресурсов компьютера для неактивных вкладок. Это может привести к закрытию SignalR подключений и может негативно сказаться на пользовательском опыте. Браузеры используют эвристики, чтобы выяснить, следует ли поместить вкладку в спящий режим, например:

  • Воспроизведение звука
  • Удержание веб-блокировки
  • IndexedDB Хранение блокировки
  • Подключение к USB-устройству
  • Запись видео или звука
  • Зеркальное отображение
  • Запись окна или отображения

Note

Эти эвристики могут меняться со временем или отличаться между браузерами. Проверьте матрицу поддержки и узнайте, какой метод лучше всего подходит для ваших сценариев.

Чтобы избежать перевода приложения в спящий режим, приложение должно задействовать одну из эвристик, используемых браузером.

В следующем примере кода показано, как использовать веб-блокировку для поддержания пробуждения вкладки и предотвращения неожиданного закрытия подключения.

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;
    });
}

В приведенном выше примере кода:

  • Веб-блокировки являются экспериментальными. Условный флажок подтверждает, что браузер поддерживает веб-блокировки.
  • Сопоставитель обещаний (lockResolver) хранится таким образом, чтобы блокировка была освобождена, когда она допустима для перехода на вкладку в спящий режим.
  • При закрытии подключения блокировка освобождается путем вызова lockResolver(). Когда блокировка снимается, вкладке разрешается перейти в спящий режим.

Дополнительные ресурсы