Blazor WebAssembly单例状态容器服务在新标签页组件中无法获取数据的问题
Blazor WebAssembly单例状态容器服务在新标签页组件中无法获取数据的问题
这个问题其实是Blazor WebAssembly的运行机制导致的,我来给你拆解下原因和解决方案:
问题根源
Blazor WebAssembly应用在每个浏览器标签页中都是独立的运行实例——哪怕你用AddSingleton注册服务,这个“单例”也只限于当前标签页的应用实例。当你打开新标签页时,浏览器会重新加载整个Blazor应用,初始化全新的服务实例,所以新标签页里的ServiceScopped自然是空的,和原标签页的服务完全不共享数据。
解决方案:用浏览器本地存储(localStorage)实现跨标签页数据同步
要让数据在多个标签页间共享,我们需要把状态持久化到浏览器的本地存储(localStorage)里,它是浏览器级别的存储,所有同域名的标签页都能访问。同时还要监听存储变化事件,确保一个标签页修改数据后,其他标签页能自动更新状态。
1. 修改状态服务,加入本地存储支持
我们需要给ServiceScopped注入IJSRuntime来操作localStorage,同时添加存储变化的监听:
using Microsoft.JSInterop; using System.Text.Json; public class ServiceScopped : IDisposable { private readonly IJSRuntime _jsRuntime; public DataDTO Data { get; private set; } = new DataDTO(); public event Action? Change; public ServiceScopped(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; // 初始化时从localStorage加载数据 _ = LoadDataFromLocalStorageAsync(); // 监听其他标签页的存储变化 _ = ListenForStorageChangesAsync(); } public async Task UpdateData(DataDTO newData) { Data = newData; // 保存到localStorage await SaveDataToLocalStorageAsync(newData); NotifyStateChanged(); } private async Task SaveDataToLocalStorageAsync(DataDTO data) { var json = JsonSerializer.Serialize(data); await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "serviceData", json); } private async Task LoadDataFromLocalStorageAsync() { var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "serviceData"); if (!string.IsNullOrEmpty(json)) { Data = JsonSerializer.Deserialize<DataDTO>(json)!; NotifyStateChanged(); } } private async Task ListenForStorageChangesAsync() { await _jsRuntime.InvokeVoidAsync("addEventListener", "storage", DotNetObjectReference.Create(this).InvokeMethodAsync<string>("OnStorageChanged")); } [JSInvokable] public void OnStorageChanged() { // 存储变化时重新加载数据 _ = LoadDataFromLocalStorageAsync(); } private void NotifyStateChanged() => Change?.Invoke(); public void Dispose() { _ = _jsRuntime.InvokeVoidAsync("removeEventListener", "storage", DotNetObjectReference.Create(this).InvokeMethodAsync<string>("OnStorageChanged")); } }
2. 调整Program.cs的服务注册
确保服务能注入IJSRuntime,注册方式不变,注意服务类已实现IDisposable:
builder.Services.AddSingleton<ServiceScopped>();
3. 调整组件代码
主组件的Save方法无需大改,现在UpdateData会自动把数据存到localStorage:
@inject ServiceScopped ServiceScopped @inject IJSRuntime jsRuntime @implements IDisposable <EditForm OnSubmit="@HandleSubmit"> <!-- 表单内容 --> </EditForm> @code { protected override void OnInitialized() { ServiceScopped.Change += StateHasChanged; } private async Task HandleSubmit() { await Save(); } private async Task Save() { var serviceDTO = new DataDTO { // 赋值表单属性 }; await ServiceScopped.UpdateData(serviceDTO); await jsRuntime.InvokeVoidAsync("open", "newwindows", "_blank"); } public void Dispose() { ServiceScopped.Change -= StateHasChanged; } }
新标签页的组件初始化时,会自动从localStorage加载最新数据:
@inject ServiceScopped ServiceScopped @implements IDisposable <table> <!-- 渲染ServiceScopped.Data的内容 --> </table> @code{ protected override void OnInitialized() { ServiceScopped.Change += StateHasChanged; } public void Dispose() { ServiceScopped.Change -= StateHasChanged; } }
额外说明
- 确保
DataDTO是可序列化的(比如没有循环引用,属性均为公共可访问)。 - 如果数据敏感,不要用localStorage,可考虑其他安全存储方案;sessionStorage是标签页隔离的,不适合跨标签页共享。
storage事件只会在其他标签页修改存储时触发当前标签页的回调,当前标签页修改不会触发,这符合我们的同步需求。
备注:内容来源于stack exchange,提问作者Hale Bopp
相关产品推荐
相关产品推荐

