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

Blazor WebAssembly中如何全局检测客户端与服务端的连接状态

Blazor WASM全局连接状态检测实现方案

1. 新增网络状态检测基础服务

首先实现浏览器原生网络状态监听,处理客户端本地断网场景:

public interface INetworkStatusService
{
    bool IsOnline { get; }
    event Action<bool> NetworkStatusChanged;
    Task InitializeAsync();
}

public class NetworkStatusService : INetworkStatusService, IAsyncDisposable
{
    private readonly IJSRuntime _jsRuntime;
    private DotNetObjectReference<NetworkStatusService>? _dotNetRef;
    public bool IsOnline { get; private set; }
    public event Action<bool>? NetworkStatusChanged;

    public NetworkStatusService(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }

    public async Task InitializeAsync()
    {
        _dotNetRef = DotNetObjectReference.Create(this);
        IsOnline = await _jsRuntime.InvokeAsync<bool>("navigator.onLine");
        await _jsRuntime.InvokeVoidAsync("window.registerNetworkStatusListeners", _dotNetRef);
    }

    [JSInvokable]
    public void OnNetworkStatusChanged(bool isOnline)
    {
        IsOnline = isOnline;
        NetworkStatusChanged?.Invoke(isOnline);
    }

    public async ValueTask DisposeAsync()
    {
        if (_dotNetRef != null)
        {
            await _jsRuntime.InvokeVoidAsync("window.removeNetworkStatusListeners", _dotNetRef);
            _dotNetRef.Dispose();
        }
    }
}

添加对应的JS监听代码,放在wwwroot/index.html的<script>标签里:

let onlineHandler, offlineHandler;
window.registerNetworkStatusListeners = (dotNetRef) => {
    onlineHandler = () => dotNetRef.invokeMethodAsync('OnNetworkStatusChanged', true);
    offlineHandler = () => dotNetRef.invokeMethodAsync('OnNetworkStatusChanged', false);
    window.addEventListener('online', onlineHandler);
    window.addEventListener('offline', offlineHandler);
};
window.removeNetworkStatusListeners = (dotNetRef) => {
    window.removeEventListener('online', onlineHandler);
    window.removeEventListener('offline', offlineHandler);
};

2. 新增SignalR后端连通性检测服务

处理客户端网络正常但后端服务停止/无法连接的场景,通过SignalR长连接心跳实现:

public interface IServerConnectionService
{
    bool IsServerConnected { get; }
    event Action<bool> ServerConnectionStatusChanged;
    Task StartAsync();
    Task StopAsync();
}

public class ServerConnectionService : IServerConnectionService, IAsyncDisposable
{
    private readonly NavigationManager _navManager;
    private HubConnection? _hubConnection;
    public bool IsServerConnected { get; private set; }
    public event Action<bool>? ServerConnectionStatusChanged;

    public ServerConnectionService(NavigationManager navManager)
    {
        _navManager = navManager;
    }

    public async Task StartAsync()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(_navManager.ToAbsoluteUri("/healthHub"))
            .WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) })
            .Build();

        _hubConnection.Closed += OnConnectionClosed;
        _hubConnection.Reconnected += OnReconnected;
        _hubConnection.Reconnecting += OnReconnecting;

        try
        {
            await _hubConnection.StartAsync();
            IsServerConnected = true;
            ServerConnectionStatusChanged?.Invoke(true);
        }
        catch
        {
            IsServerConnected = false;
            ServerConnectionStatusChanged?.Invoke(false);
        }
    }

    private Task OnReconnecting(Exception? arg)
    {
        IsServerConnected = false;
        ServerConnectionStatusChanged?.Invoke(false);
        return Task.CompletedTask;
    }

    private Task OnReconnected(string? arg)
    {
        IsServerConnected = true;
        ServerConnectionStatusChanged?.Invoke(true);
        return Task.CompletedTask;
    }

    private Task OnConnectionClosed(Exception? arg)
    {
        IsServerConnected = false;
        ServerConnectionStatusChanged?.Invoke(false);
        return Task.CompletedTask;
    }

    public async Task StopAsync()
    {
        if (_hubConnection != null)
        {
            await _hubConnection.StopAsync();
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_hubConnection != null)
        {
            await _hubConnection.DisposeAsync();
        }
    }
}
  • 后端需要同步添加一个空的HealthHub,不需要实现任何方法,仅用于维持连接:
// 后端ASP.NET Core项目中添加
public class HealthHub : Hub
{
}
// Program.cs中注册
builder.Services.AddSignalR();
// 中间件配置部分添加
app.MapHub<HealthHub>("/healthHub");

3. 改造现有HttpInterceptorService

整合上述两个服务的状态,处理连接丢失场景:

public class HttpInterceptorService
{
    private readonly HttpClientInterceptor _interceptor;
    private readonly NavigationManager _navManager;
    private readonly RefreshTokenService _refreshTokenService;
    // 新增注入
    private readonly INetworkStatusService _networkStatusService;
    private readonly IServerConnectionService _serverConnectionService;
    
    public HttpInterceptorService(HttpClientInterceptor interceptor,
        NavigationManager navManager, 
        RefreshTokenService refreshTokenService,
        INetworkStatusService networkStatusService,
        IServerConnectionService serverConnectionService)
    {
        _interceptor = interceptor;
        _navManager = navManager;               
        _refreshTokenService = refreshTokenService;
        _networkStatusService = networkStatusService;
        _serverConnectionService = serverConnectionService;

        // 监听状态变化,全局提示或跳转
        _networkStatusService.NetworkStatusChanged += OnStatusChanged;
        _serverConnectionService.ServerConnectionStatusChanged += OnStatusChanged;
    }

    private void OnStatusChanged(bool isConnected)
    {
        if (!isConnected)
        {
            _navManager.NavigateTo("/PageError?message=网络连接已断开,请检查网络或稍后重试");
        }
    }

    public void RegisterEvent() => _interceptor.AfterSend += HandleResponse;
    public void RegisterBeforeSendEvent() =>
        _interceptor.BeforeSendAsync += InterceptBeforeSendAsync;

    public void DisposeEvent()
    {
        _interceptor.AfterSend -= HandleResponse;
        _interceptor.BeforeSendAsync -= InterceptBeforeSendAsync;
        _networkStatusService.NetworkStatusChanged -= OnStatusChanged;
        _serverConnectionService.ServerConnectionStatusChanged -= OnStatusChanged;
    }

    private async Task InterceptBeforeSendAsync(object sender,
        HttpClientInterceptorEventArgs e)
    {
        // 发送请求前先检查连接状态,已断开直接拦截
        if (!_networkStatusService.IsOnline || !_serverConnectionService.IsServerConnected)
        {
            _navManager.NavigateTo("/PageError?message=网络连接不可用,请检查后重试");
            throw new HttpResponseException("网络连接不可用");
        }

        var absolutePath = e.Request.RequestUri.AbsolutePath;
        if (!absolutePath.Contains("token") && !absolutePath.Contains("account"))
        {
            var token = await _refreshTokenService.TryRefreshToken();
            if (!string.IsNullOrEmpty(token))
            {
                e.Request.Headers.Authorization =
                    new AuthenticationHeaderValue("bearer", token);
            }
        }
    }

    private void HandleResponse(object sender, HttpClientInterceptorEventArgs e)
    {
        if (e.Response == null)
        {
            _navManager.NavigateTo("/PageError?message=服务器无法连接,请稍后重试");
            throw new HttpResponseException("Server not available.");
        }

        var message = "";
        if (!e.Response.IsSuccessStatusCode)
        {
            switch (e.Response.StatusCode)
            {
                case HttpStatusCode.NotFound:
                    _navManager.NavigateTo("/Page404");                 
                    break;
                case HttpStatusCode.BadRequest:                                 
                    break;
                case HttpStatusCode.Unauthorized:
                    _navManager.NavigateTo("/unauthorized");                    
                    break;
                default:
                    _navManager.NavigateTo("/PageError");                   
                    break;
            }
            throw new HttpResponseException(message);
        }
    }       
}

4. 全局注册和初始化

在Blazor WASM项目的Program.cs中注册服务:

builder.Services.AddSingleton<INetworkStatusService, NetworkStatusService>();
builder.Services.AddSingleton<IServerConnectionService, ServerConnectionService>();
builder.Services.AddScoped<HttpInterceptorService>();
// 原有HttpClientInterceptor等注册保留

在App.razor中初始化服务,保证全局生效:

@inject INetworkStatusService NetworkStatusService
@inject IServerConnectionService ServerConnectionService
@inject HttpInterceptorService HttpInterceptorService
@implements IAsyncDisposable

<Router AppAssembly="@typeof(App).Assembly">
    <!-- 原有路由配置保留 -->
</Router>

@code {
    protected override async Task OnInitializedAsync()
    {
        await NetworkStatusService.InitializeAsync();
        await ServerConnectionService.StartAsync();
        HttpInterceptorService.RegisterBeforeSendEvent();
        HttpInterceptorService.RegisterEvent();
    }

    public async ValueTask DisposeAsync()
    {
        HttpInterceptorService.DisposeEvent();
        await NetworkStatusService.DisposeAsync();
        await ServerConnectionService.DisposeAsync();
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 08:24:04