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

Blazor Server+SignalR多标签页单连接管理方案咨询

同一用户多标签页共享单个SignalR连接解决方案

核心思路是利用浏览器Shared Worker实现跨标签页的连接共享——Shared Worker是同源页面共享的后台脚本,所有标签页都通过它和SignalR服务器通信,这样不管开多少标签,只会存在一个SignalR连接。同时结合用户标识(登录用户用ID,未登录用户用浏览器唯一UUID)实现精准推送。

客户端实现步骤

1. 创建Shared Worker脚本(signalr-worker.js)

这个脚本负责管理唯一的SignalR连接,处理和服务器的通信,以及和各个标签页的消息交互:

const connections = new Map(); // 存储各个标签页的消息端口
let hubConnection = null;
let currentUserIdentifier = null;

// 监听标签页的连接请求
self.addEventListener('connect', (e) => {
  const port = e.ports[0];
  port.addEventListener('message', (msg) => {
    switch(msg.type) {
      case 'init':
        currentUserIdentifier = msg.data.userIdentifier;
        initSignalRConnection();
        connections.set(port, port);
        break;
      case 'subscribe':
        hubConnection?.invoke('SubscribeToUpdates', msg.data.topic);
        break;
      case 'unsubscribe':
        hubConnection?.invoke('UnsubscribeFromUpdates', msg.data.topic);
        break;
    }
  });
  port.start();
});

// 初始化SignalR连接
function initSignalRConnection() {
  if (hubConnection?.state === 'Connected') return;
  
  const signalR = require('@microsoft/signalr');
  hubConnection = new signalR.HubConnectionBuilder()
    .withUrl('/updatesHub')
    .withAutomaticReconnect()
    .build();

  // 接收服务器推送,转发给所有标签页
  hubConnection.on('ReceiveUpdate', (updateData) => {
    connections.forEach(port => port.postMessage({ type: 'update', data: updateData }));
  });

  // 连接建立时,向服务器注册用户标识
  hubConnection.on('connected', () => {
    hubConnection.invoke('RegisterUser', currentUserIdentifier);
  });

  // 连接断开时清理
  hubConnection.onclose(() => {
    connections.clear();
    hubConnection = null;
  });

  hubConnection.start().catch(err => console.error('SignalR连接失败:', err));
}

2. Blazor客户端集成Shared Worker

在Blazor组件中,通过IJSObjectReference和Shared Worker通信,接收推送并更新UI:

@inject IJSRuntime JSRuntime
@implements IAsyncDisposable

private IJSObjectReference? _workerPort;
private string _userIdentifier = string.Empty;

protected override async Task OnInitializedAsync() {
  // 获取用户标识:登录用户取UserId,未登录用户生成UUID存localStorage
  var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
  var isAuthenticated = authState.User.Identity?.IsAuthenticated ?? false;
  if (isAuthenticated) {
    _userIdentifier = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
  } else {
    _userIdentifier = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "anonymousUserId");
    if (string.IsNullOrEmpty(_userIdentifier)) {
      _userIdentifier = Guid.NewGuid().ToString();
      await JSRuntime.InvokeVoidAsync("localStorage.setItem", "anonymousUserId", _userIdentifier);
    }
  }

  // 连接Shared Worker
  _workerPort = await JSRuntime.InvokeAsync<IJSObjectReference>("connectToSignalRWorker", _userIdentifier);
  
  // 监听Worker转发的更新
  await _workerPort.InvokeVoidAsync("listenForUpdates", DotNetObjectReference.Create(this));
}

[JSInvokable]
public void HandleUpdate(string updateData) {
  // 这里处理UI更新,比如调用StateHasChanged()
  StateHasChanged();
}

public async ValueTask DisposeAsync() {
  if (_workerPort != null) {
    await _workerPort.DisposeAsync();
  }
}

配套的JavaScript辅助函数(放在wwwroot/js/utils.js):

function connectToSignalRWorker(userIdentifier) {
  const worker = new SharedWorker('/signalr-worker.js');
  worker.port.postMessage({ type: 'init', data: { userIdentifier } });
  
  return {
    listenForUpdates(dotNetRef) {
      worker.port.addEventListener('message', (msg) => {
        if (msg.type === 'update') {
          dotNetRef.invokeMethodAsync('HandleUpdate', JSON.stringify(msg.data));
        }
      });
    },
    subscribe(topic) {
      worker.port.postMessage({ type: 'subscribe', data: { topic } });
    }
  };
}

服务器端实现步骤

1. 维护用户标识到ConnectionId的映射

在SignalR Hub中使用线程安全的字典管理映射:

public class UpdatesHub : Hub {
  private static readonly ConcurrentDictionary<string, string> _userToConnectionMap = new();

  public async Task RegisterUser(string userIdentifier) {
    if (!string.IsNullOrEmpty(userIdentifier)) {
      // 移除该用户之前的连接(如果有)
      if (_userToConnectionMap.TryGetValue(userIdentifier, out var oldConnectionId)) {
        await Groups.RemoveFromGroupAsync(oldConnectionId, userIdentifier);
      }
      // 更新映射并加入用户分组
      _userToConnectionMap[userIdentifier] = Context.ConnectionId;
      await Groups.AddToGroupAsync(Context.ConnectionId, userIdentifier);
    }
  }

  // 推送时直接针对用户分组发送
  public async Task SendUpdateToUser(string userIdentifier, object updateData) {
    await Clients.Group(userIdentifier).SendAsync("ReceiveUpdate", updateData);
  }

  public override async Task OnDisconnectedAsync(Exception? exception) {
    // 移除断开连接的用户映射
    var entry = _userToConnectionMap.FirstOrDefault(kv => kv.Value == Context.ConnectionId);
    if (!string.IsNullOrEmpty(entry.Key)) {
      _userToConnectionMap.TryRemove(entry.Key, out _);
      await Groups.RemoveFromGroupAsync(Context.ConnectionId, entry.Key);
    }
    await base.OnDisconnectedAsync(exception);
  }
}

2. 配置SignalR和Shared Worker

在Program.cs中确保SignalR路由正确:

app.MapHub<UpdatesHub>("/updatesHub");
// 确保Shared Worker脚本可以被访问
app.UseStaticFiles();

关键注意事项

  • 同源限制:Shared Worker仅支持同源页面,跨域名场景需要额外配置CORS和Worker跨域策略。
  • 登录状态切换:当用户从匿名转为登录时,需要通知Worker更新用户标识,重新向服务器注册。
  • 连接重连:SignalR的自动重连机制会在Worker中自动处理,无需每个标签页单独处理。
  • 资源清理:当所有标签页关闭时,Shared Worker会自动终止,SignalR连接也会断开。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 12:45:34