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

Blazor使用SignalR Hub强制刷新标签页时的TaskCanceledException问题

Blazor SignalR多标签页强制刷新:连接管理与异常修复方案

核心问题根源

你的问题本质是连接实例重复创建、Circuit回收延迟、未主动处理连接中断异常、静态方法共享实例导致释放失效这几个点交织导致的。下面是针对性的分步解决方案:

一、实现单标签页唯一SignalR连接

将SignalR连接管理封装为Scoped服务,确保每个标签页(Blazor Circuit)对应唯一连接实例,避免重复订阅消息导致多倍推送。

1. 创建Scoped连接管理服务

public class HubConnectionManager : IAsyncDisposable
{
    private readonly string _circuitId;
    public HubConnection Connection { get; private set; }

    public HubConnectionManager(NavigationManager navigationManager, IHttpContextAccessor httpContextAccessor)
    {
        // 用CircuitId作为连接标识,方便Hub端追踪
        _circuitId = httpContextAccessor.HttpContext?.Features.Get<CircuitFeature>()?.CircuitId 
                     ?? Guid.NewGuid().ToString();

        Connection = new HubConnectionBuilder()
            .WithUrl(navigationManager.ToAbsoluteUri("/refreshHub"), options =>
            {
                options.Headers.Add("X-Circuit-Id", _circuitId);
            })
            .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5) })
            .Build();
    }

    public async Task StartAsync()
    {
        if (Connection.State == HubConnectionState.Disconnected)
        {
            try
            {
                await Connection.StartAsync();
            }
            catch (TaskCanceledException)
            {
                // 静默处理连接超时/取消,避免日志刷屏
                Console.WriteLine($"SignalR连接启动取消(Circuit: {_circuitId})");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"SignalR连接失败(Circuit: {_circuitId}): {ex.Message}");
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (Connection.State != HubConnectionState.Disconnected)
        {
            try
            {
                await Connection.StopAsync();
            }
            catch { }
            finally
            {
                await Connection.DisposeAsync();
            }
        }
    }
}

2. 注册服务并在MainLayout中注入

在Program.cs注册Scoped服务:

builder.Services.AddScoped<HubConnectionManager>();
builder.Services.AddHttpContextAccessor(); // 用于获取CircuitId

修改MainLayout.razor,注入服务并初始化连接:

@inject HubConnectionManager HubManager
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable

@code {
    private DotNetObjectReference<MainLayout>? _dotNetRef;

    protected override async Task OnInitializedAsync()
    {
        await HubManager.StartAsync();
        // 订阅强制刷新消息
        HubManager.Connection.On("ForceRefresh", async () =>
        {
            try
            {
                // 强制重载当前页面,触发权限重新加载
                NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
            }
            catch (TaskCanceledException)
            {
                Console.WriteLine("页面刷新任务被取消");
            }
        });
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 绑定当前组件实例,用于JS互调
            _dotNetRef = DotNetObjectReference.Create(this);
            await JSRuntime.InvokeVoidAsync("registerUnloadHandler", _dotNetRef);
        }
    }

    // JS调用的页面卸载处理方法
    [JSInvokable]
    public async Task OnPageUnload()
    {
        await HubManager.DisposeAsync();
    }

    public async ValueTask DisposeAsync()
    {
        _dotNetRef?.Dispose();
        await HubManager.DisposeAsync();
    }
}

二、页面关闭/刷新时立即释放连接

通过JS监听beforeunload事件,主动通知.NET端销毁连接,绕过Blazor默认3分钟的Circuit回收延迟。

在wwwroot/js/site.js中添加:

window.registerUnloadHandler = (dotNetRef) => {
    window.addEventListener('beforeunload', async () => {
        try {
            await dotNetRef.invokeMethodAsync('OnPageUnload');
        } catch (e) {
            console.log("释放连接失败:", e);
        } finally {
            dotNetRef.dispose();
        }
    });
};

三、解决TaskCanceledException异常

这类异常大多是连接中断、自动重连取消或导航操作超时导致的,通过以下方式处理:

  • 在连接启动、消息处理中捕获并静默处理TaskCanceledException,避免日志刷屏;
  • 确保DisposeAsync中正确停止并销毁连接,阻止后续自动重连尝试;
  • 配置合理的自动重连策略,避免无意义的重试。

四、修复JS互调仅最后标签页生效的问题

之前使用静态方法的问题是静态成员共享状态,所有标签页共用同一个实例。现在通过DotNetObjectReference绑定每个MainLayout组件实例,JS调用的是当前标签页的专属方法,彻底解决实例冲突问题。

额外优化建议

  • 在Hub端记录每个连接的X-Circuit-Id,方便排查多标签连接问题;
  • 定期清理Hub端的无效连接(比如通过定时任务检查连接状态);
  • 避免在SignalR消息回调中执行长时间同步操作,尽量异步化处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 12:50:56