.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
相关产品推荐
相关产品推荐

