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

Blazor Wasm服务中如何正确实现异步初始化逻辑?

Blazor WASM 连接状态单例服务的异步初始化方案

问题背景

我在Blazor WASM项目中实现了一个单例Connection Service,通过JS Interop监听online和offline事件跟踪网络连接状态,供多个组件使用。当前代码能运行,但在构造函数中调用异步方法并丢弃ValueTask的做法明显不规范。

原服务代码:

public class ConnectionService : IAsyncDisposable
{
    private readonly IJSRuntime JsRuntime;
    public bool IsOnline { get; set; } = true;
    public event Action ConnectionStatusChanged;

    public ConnectionService(IJSRuntime jsRuntime)
    {
        JsRuntime = jsRuntime;
        _ = Initialize();
    }

    public ValueTask Initialize()
    {
        return JsRuntime.InvokeVoidAsync("Connection.Initialize", DotNetObjectReference.Create(this));
    }

    [JSInvokable("Connection.StatusChanged")]
    public void OnJsConnectionStatusChanged(bool isOnline)
    {
        if (IsOnline == isOnline) return;
        IsOnline = isOnline;
        ConnectionStatusChanged?.Invoke();
    }

    public async ValueTask DisposeAsync()
    {
        await JsRuntime.InvokeVoidAsync("Connection.Dispose");
    }
}

直接提前实例化并初始化的方式不可行,因为服务依赖IJSRuntime,无法在注册前获取该实例:

var connSingleton = new ConnectionService();
await connSingleton.Initialize();
builder.Services.AddSingleton<ConnectionService>(connSingleton);

正确解决方案

方案1:异步工厂方法注册单例

Blazor WASM的服务容器支持通过异步工厂创建单例实例,能在注册阶段完成异步初始化,避免构造函数的异步调用问题。

注册代码:

builder.Services.AddSingleton<ConnectionService>(async sp =>
{
    var jsRuntime = sp.GetRequiredService<IJSRuntime>();
    var service = new ConnectionService(jsRuntime);
    await service.Initialize();
    return service;
});

优势:服务容器会等待初始化完成后,再将实例注入到组件中,确保组件拿到的是已完成初始化的服务实例,不会出现状态不一致的情况。


方案2:延迟初始化(Lazy Initialization)

如果希望服务在第一次被使用时才完成初始化,可以结合Lazy<T>实现线程安全的延迟异步初始化,减少应用启动时的开销。

修改后的服务代码:

public class ConnectionService : IAsyncDisposable
{
    private readonly IJSRuntime _jsRuntime;
    private readonly Lazy<Task> _initializationTask;
    public bool IsOnline { get; set; } = true;
    public event Action ConnectionStatusChanged;

    public ConnectionService(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
        // 用Lazy包装初始化逻辑,确保只执行一次
        _initializationTask = new Lazy<Task>(async () =>
        {
            await _jsRuntime.InvokeVoidAsync("Connection.Initialize", DotNetObjectReference.Create(this));
        });
    }

    // 对外暴露方法,供调用者等待初始化完成
    public Task EnsureInitialized()
    {
        return _initializationTask.Value;
    }

    [JSInvokable("Connection.StatusChanged")]
    public void OnJsConnectionStatusChanged(bool isOnline)
    {
        if (IsOnline == isOnline) return;
        IsOnline = isOnline;
        ConnectionStatusChanged?.Invoke();
    }

    public async ValueTask DisposeAsync()
    {
        // 确保初始化任务完成后再释放资源
        if (_initializationTask.IsValueCreated)
        {
            await _initializationTask.Value;
        }
        await _jsRuntime.InvokeVoidAsync("Connection.Dispose");
    }
}

注册方式保持常规单例注册:

builder.Services.AddSingleton<ConnectionService>();

组件中使用时,在初始化阶段等待服务完成初始化:

@inject ConnectionService _connectionService

@code {
    protected override async Task OnInitializedAsync()
    {
        await _connectionService.EnsureInitialized();
        // 后续业务逻辑
    }
}

关键注意事项

  • 禁止在构造函数中执行异步操作:构造函数是同步执行的,强行调用异步方法会导致任务未被等待,引发状态不一致或异常无法捕获的问题。
  • 管理DotNetObjectReference生命周期:由于单例服务会在应用生命周期内持续存在,正好满足JS侧长期持有引用的需求,无需额外处理引用释放(除非手动调用Dispose)。
  • 异常处理:两种方案都建议添加异常捕获逻辑,比如在工厂方法或Lazy初始化中捕获初始化异常,避免服务注册失败或组件启动报错。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 05:32:41