You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 7 SignalR:能否抽象Hub以在多个Blazor页面复用?

问题背景

我有一个运行良好的Hub,目前包含3个事件:

  • ReceiveMessage(直接取自聊天应用示例)
  • RefreshPage(用于在一个页面使另一个页面失效时)
  • NotificationsUpdated(显示当前通知数量的图标)

目前Chat页面运行正常,我在该页面捕获这3个事件以验证功能可用(确实可用)。但我知道站点中的部分页面无需连接此Hub,而另一些页面仅需处理1-2个事件。我最终可能会删除Chat的ReceiveMessage事件。

目前我是这样使用HubConnectionBuilder的:

protected override async Task OnInitializedAsync()
{
  hubConnection = new HubConnectionBuilder()
    .WithUrl(NavigationManager.ToAbsoluteUri("/hub/ping"), options =>
    {
      options.AccessTokenProvider = async () => await GetAccessTokenValueAsync();
    })
    .WithAutomaticReconnect()
    .Build();

  hubRegistrations.Add(hubConnection.OnMessageReceived(OnMessageReceivedAsync));
  hubRegistrations.Add(hubConnection.OnRefreshPage(OnRefreshPageAsync));
  hubRegistrations.Add(hubConnection.OnNotificationsUpdate(OnNotificationsUpdateAsync));

  await hubConnection.StartAsync();
}

以下是其中一个事件的示例:

public async Task OnMessageReceivedAsync(string user, string message) =>
  await InvokeAsync(() =>
  {
    var encodedMessage = $"{user}: {message}";
    Messages.Add(encodedMessage);
    StateHasChanged();
  });

如果要在其他页面实现,会有大量重复代码(即使不需要实现所有3个事件,重复代码也不少)。

[Inject]
public required NavigationManager NavigationManager { get; set; }

[Inject]
public required IAccessTokenProvider TokenProvider { get; set; }

[Inject]
public required StateManager State { get; set; }

private HubConnection hubConnection;
private readonly HashSet<IDisposable> hubRegistrations = new();

public async ValueTask DisposeAsync()
{
  await PHST.DisposeAsync();
  if (hubRegistrations is { Count: > 0 })
  {
    foreach (var disposable in hubRegistrations)
      disposable.Dispose();
  }

  if (hubConnection is not null)
  {
    await hubConnection.DisposeAsync();
  }
}

核心问题

作为开发者,我希望为后续开发者和页面简化实现,想将其做成服务或单例等形式。这样可行吗?或者有没有更好的方式在多个页面共用一个Hub?

我尝试过制作基类,但对于已有继承关系的页面来说,继承会很麻烦。我刚尝试了一点可注入的服务,但不想贸然深入,所以先来提问。

我还希望它能动态处理事件,让事件可以根据给定数据执行相应操作。目前我通过一些扩展方法实现了动态处理:

public static IDisposable OnMessageReceived(this HubConnection connection, Func<string, string, Task> handler) =>
  connection.On("ReceiveMessage", handler);

public static IDisposable OnRefreshPage(this HubConnection connection, Func<string, Task> handler) =>
  connection.On("RefreshPage", handler);

public static IDisposable OnNotificationsUpdate(this HubConnection connection, Func<int, Task> handler) =>
  connection.On("NotificationsUpdate", handler);

解决方案

1. 封装成单例服务(推荐)

完全可以将Hub连接封装成可注入的单例服务,整个应用内只会维持一个Hub连接,避免重复创建连接,同时统一管理事件注册和生命周期。

步骤1:创建Hub服务类

public interface IPingHubService
{
    HubConnection Connection { get; }
    Task StartAsync();
    IDisposable RegisterMessageReceivedHandler(Func<string, string, Task> handler);
    IDisposable RegisterRefreshPageHandler(Func<string, Task> handler);
    IDisposable RegisterNotificationsUpdateHandler(Func<int, Task> handler);
    void UnregisterHandler(IDisposable registration);
}

public class PingHubService : IPingHubService, IAsyncDisposable
{
    private readonly HubConnection _hubConnection;
    private readonly HashSet<IDisposable> _registrations = new();

    public HubConnection Connection => _hubConnection;

    public PingHubService(NavigationManager navigationManager, IAccessTokenProvider tokenProvider)
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(navigationManager.ToAbsoluteUri("/hub/ping"), options =>
            {
                options.AccessTokenProvider = async () =>
                {
                    var tokenResult = await tokenProvider.RequestAccessToken();
                    return tokenResult.TryGetToken(out var token) ? token.Value : string.Empty;
                };
            })
            .WithAutomaticReconnect()
            .Build();
    }

    public async Task StartAsync()
    {
        if (_hubConnection.State != HubConnectionState.Connected)
        {
            await _hubConnection.StartAsync();
        }
    }

    public IDisposable RegisterMessageReceivedHandler(Func<string, string, Task> handler)
    {
        var registration = _hubConnection.OnMessageReceived(handler);
        _registrations.Add(registration);
        return registration;
    }

    public IDisposable RegisterRefreshPageHandler(Func<string, Task> handler)
    {
        var registration = _hubConnection.OnRefreshPage(handler);
        _registrations.Add(registration);
        return registration;
    }

    public IDisposable RegisterNotificationsUpdateHandler(Func<int, Task> handler)
    {
        var registration = _hubConnection.OnNotificationsUpdate(handler);
        _registrations.Add(registration);
        return registration;
    }

    public void UnregisterHandler(IDisposable registration)
    {
        if (_registrations.Remove(registration))
        {
            registration.Dispose();
        }
    }

    public async ValueTask DisposeAsync()
    {
        foreach (var registration in _registrations)
        {
            registration.Dispose();
        }
        _registrations.Clear();

        if (_hubConnection.State != HubConnectionState.Disconnected)
        {
            await _hubConnection.DisposeAsync();
        }
    }
}

步骤2:注册服务

在Program.cs中将服务注册为单例:

builder.Services.AddSingleton<IPingHubService, PingHubService>();

步骤3:在页面中使用

[Inject]
public required IPingHubService PingHubService { get; set; }

private IDisposable _messageRegistration;
private IDisposable _refreshRegistration;

protected override async Task OnInitializedAsync()
{
    await PingHubService.StartAsync();

    // 只注册当前页面需要的事件
    _messageRegistration = PingHubService.RegisterMessageReceivedHandler(OnMessageReceivedAsync);
    _refreshRegistration = PingHubService.RegisterRefreshPageHandler(OnRefreshPageAsync);
}

public async ValueTask DisposeAsync()
{
    // 注销当前页面的事件处理器
    PingHubService.UnregisterHandler(_messageRegistration);
    PingHubService.UnregisterHandler(_refreshRegistration);
}

// 事件处理方法
public async Task OnMessageReceivedAsync(string user, string message)
{
    await InvokeAsync(() =>
    {
        var encodedMessage = $"{user}: {message}";
        Messages.Add(encodedMessage);
        StateHasChanged();
    });
}

2. 替代方案:组件级事件总线(松散耦合场景)

如果想进一步解耦Hub和页面,可以结合事件总线模式:Hub服务接收事件后,将事件转发到应用内的事件总线,页面只需订阅事件总线的对应事件即可。这种方式适合事件较多、页面逻辑独立的场景,但会增加一层复杂度。

关键优势

  • 消除重复代码:Hub连接的初始化、认证、重连逻辑统一维护,页面只需关注自身业务事件。
  • 单例连接复用:整个应用维持一个WebSocket连接,降低服务器资源消耗。
  • 灵活的事件管理:页面按需注册/注销事件,无需强制依赖所有Hub事件。
  • 规范生命周期:服务实现IAsyncDisposable,确保应用关闭时正确释放连接和资源。

内容的提问来源于stack exchange,提问作者Grandizer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 08:42:36