Blazor WebAssembly 中的 JSRuntime 在首次加载时阻止 DOM 更新
JSRuntime in Blazor WebAssembly blocking DOM updates on first load
我用的是最新的Blazor WebAssembly 3.2.0 Release Candidate
我从 vanilla Blazor WASM 模板中对 test/debug 问题进行了以下调整:
在Program.cs中,我添加了以下将注入所有页面的单例:
builder.Services.AddSingleton<ApplicationState>();
class 看起来像:
public class ApplicationState
{
public bool BoolValue { get; set; }
public string StringValue { get; set; }
public ApplicationState()
{
BoolValue = false;
StringValue = "Default value";
}
}
并与 JSRuntime 一起注入 _Imports.razor,因此它们可用于所有页面:
@inject IJSRuntime JSRuntime
@inject BlazorApp1.State.ApplicationState AppState;
为了测试 ApplicationState object 是否正确地通过应用程序,我在以下 2 个位置使用了它:
在 NavManu.razor 中,我将菜单项之一围绕 BoolValue 包装到 show/hide 那个项目:
@if (AppState.BoolValue)
{
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
}
并且在 Index.razor 我在默认的 Hello World header 下添加了一个 H2 标签来显示 StringValue
<h1>Hello, world!</h1>
<h2>@AppState.StringValue</h2>
我还有一个 javascript 文件位于 wwwroot/js/extenstions.js 它有一个用于获取 cookie 数据的函数:
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
};
并作为脚本包含在 wwwroot/index.html 中 _framework/blazor.webassembly.js 脚本:
<script src="_framework/blazor.webassembly.js"></script>
<script src="js/extensions.js"></script>
在App.razor中我有以下@code块:
@code
{
protected override async Task OnInitializedAsync()
{
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
}
}
一切正常,菜单项以及 H2 标签中的字符串值都可见。
但是,如果我像这样通过 JSRuntime 添加对我的 Javasript 函数的调用:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
}
}
javascript 运行,AppState 值更新(我添加了一个控制台日志来测试这个)但是 导航没有更新以显示菜单项并且 H2 标签不显示新的字符串值...
如果您导航到菜单中的页面,状态将更新,但如果您进行硬刷新,则不会再次更新....
我尝试添加 StateHasChanged() 调用,但这没有用:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
StateHasChanged(); // Does not work!
}
}
由于状态似乎在导航后更新,我也尝试强制导航事件,但也无济于事:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
var uri = new Uri(NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
NavigationManager.NavigateTo(uri);
}
}
我正在继续尝试其他一些方法,但似乎没有任何效果。 StateHasChanged() 根据我的理解,确实应该解决此类问题。还有什么我应该尝试的吗?
更新
正如 enet 指出的那样,我需要在状态发生变化时通知下游组件。导致问题的不是 JSRuntime - 但它在 App.razor 中创建的延迟导致 object 状态在硬刷新时似乎有所不同。在第一种情况下,object 在其他组件开始渲染之前更新,因此它们使用已经更新的状态。但是一旦出现任何类型的延迟(例如调用 JSRuntime),他们就需要一个通知来更新他们的状态,因为他们有机会在延迟之前使用旧状态进行渲染。我用几毫秒的 Task.Delay() 替换了 JSRuntime 调用,结果出现了同样的问题!向事件添加触发器解决了这个问题。
MessageService.cs
public class MessageService
{
private string message;
public string Message
{
get => message;
set
{
if (message != value)
{
message = value;
if (Notify != null)
{
Notify?.Invoke();
}
}
}
}
public event Action Notify;
}
注意:该服务是一个普通的class...它为其他对象提供服务,应该在Startup.ConfigureServices方法中添加到DI容器中,以供请求的客户端使用.将此添加到 ConfigureServices 方法:
services.AddScoped<MessageService>();
注意:如您所见,我定义了一个 Action 类型的事件委托,当用户在 Component3 中的文本框中键入文本时,它会从 属性 的设置访问器调用。触发此委托会导致 Components3 输入的文本显示在作为 Component2 的父级的 Index 组件中(请参见下面的代码)。
Index.razor
@page "/"
@inject MessageService MessageService
@implements IDisposable
@inject IJSRuntime JSRuntime
<p>
I'm the parent of Component2. I've got a message from my grand
child: @MessageService.Message
</p>
<Component2 />
<p>@jwtToken</p>
@code {
private string jwtToken;
protected override async Task OnInitializedAsync()
{
// await SetTokenAsync("my token");
jwtToken = await GetTokenAsync();
MessageService.Notify += OnNotify;
MessageService.Message = "A message from Index";
}
public void OnNotify()
{
InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
MessageService.Notify -= OnNotify;
}
public async Task<string> GetTokenAsync()
=> await JSRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");
public async Task SetTokenAsync(string token)
{
if (token == null)
{
await JSRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
}
else
{
await JSRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
}
}
}
注意,我们直接绑定到MessageService.Message属性,但是必须调用StateHasChanged方法来刷新文本的显示。
Component2.razor
<h3>Component2: I'm the parent of component three</h3>
<Component3/>
@code {
}
Component3.razor
@inject MessageService MessageService
<p>This is component3. Please type a message to my grand parent</p>
<input placeholder="Type a message to grandpa..." type="text"
@bind="@MessageService.Message" @bind:event="oninput" />
@code {
protected override void OnInitialized()
{
MessageService.Notify += () => InvokeAsync(() =>
{
StateHasChanged();
});
}
}
请注意,在 Component3 中,我们将 MessageService.Message 绑定到文本框,并且每次按下键盘时都会发生绑定(输入事件与更改
事件)。
就是这样,希望对您有所帮助,如有任何问题,请随时提出。
我用的是最新的Blazor WebAssembly 3.2.0 Release Candidate
我从 vanilla Blazor WASM 模板中对 test/debug 问题进行了以下调整:
在Program.cs中,我添加了以下将注入所有页面的单例:
builder.Services.AddSingleton<ApplicationState>();
class 看起来像:
public class ApplicationState
{
public bool BoolValue { get; set; }
public string StringValue { get; set; }
public ApplicationState()
{
BoolValue = false;
StringValue = "Default value";
}
}
并与 JSRuntime 一起注入 _Imports.razor,因此它们可用于所有页面:
@inject IJSRuntime JSRuntime
@inject BlazorApp1.State.ApplicationState AppState;
为了测试 ApplicationState object 是否正确地通过应用程序,我在以下 2 个位置使用了它:
在 NavManu.razor 中,我将菜单项之一围绕 BoolValue 包装到 show/hide 那个项目:
@if (AppState.BoolValue)
{
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
}
并且在 Index.razor 我在默认的 Hello World header 下添加了一个 H2 标签来显示 StringValue
<h1>Hello, world!</h1>
<h2>@AppState.StringValue</h2>
我还有一个 javascript 文件位于 wwwroot/js/extenstions.js 它有一个用于获取 cookie 数据的函数:
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
};
并作为脚本包含在 wwwroot/index.html 中 _framework/blazor.webassembly.js 脚本:
<script src="_framework/blazor.webassembly.js"></script>
<script src="js/extensions.js"></script>
在App.razor中我有以下@code块:
@code
{
protected override async Task OnInitializedAsync()
{
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
}
}
一切正常,菜单项以及 H2 标签中的字符串值都可见。
但是,如果我像这样通过 JSRuntime 添加对我的 Javasript 函数的调用:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
}
}
javascript 运行,AppState 值更新(我添加了一个控制台日志来测试这个)但是 导航没有更新以显示菜单项并且 H2 标签不显示新的字符串值...
如果您导航到菜单中的页面,状态将更新,但如果您进行硬刷新,则不会再次更新....
我尝试添加 StateHasChanged() 调用,但这没有用:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
StateHasChanged(); // Does not work!
}
}
由于状态似乎在导航后更新,我也尝试强制导航事件,但也无济于事:
@code
{
protected override async Task OnInitializedAsync()
{
var jwtToken = await JSRuntime.InvokeAsync<string>("getCookie", "cookieName");
AppState.BoolValue = true;
AppState.StringValue = "Testing 123...";
var uri = new Uri(NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
NavigationManager.NavigateTo(uri);
}
}
我正在继续尝试其他一些方法,但似乎没有任何效果。 StateHasChanged() 根据我的理解,确实应该解决此类问题。还有什么我应该尝试的吗?
更新
正如 enet 指出的那样,我需要在状态发生变化时通知下游组件。导致问题的不是 JSRuntime - 但它在 App.razor 中创建的延迟导致 object 状态在硬刷新时似乎有所不同。在第一种情况下,object 在其他组件开始渲染之前更新,因此它们使用已经更新的状态。但是一旦出现任何类型的延迟(例如调用 JSRuntime),他们就需要一个通知来更新他们的状态,因为他们有机会在延迟之前使用旧状态进行渲染。我用几毫秒的 Task.Delay() 替换了 JSRuntime 调用,结果出现了同样的问题!向事件添加触发器解决了这个问题。
MessageService.cs
public class MessageService
{
private string message;
public string Message
{
get => message;
set
{
if (message != value)
{
message = value;
if (Notify != null)
{
Notify?.Invoke();
}
}
}
}
public event Action Notify;
}
注意:该服务是一个普通的class...它为其他对象提供服务,应该在Startup.ConfigureServices方法中添加到DI容器中,以供请求的客户端使用.将此添加到 ConfigureServices 方法:
services.AddScoped<MessageService>();
注意:如您所见,我定义了一个 Action 类型的事件委托,当用户在 Component3 中的文本框中键入文本时,它会从 属性 的设置访问器调用。触发此委托会导致 Components3 输入的文本显示在作为 Component2 的父级的 Index 组件中(请参见下面的代码)。
Index.razor
@page "/"
@inject MessageService MessageService
@implements IDisposable
@inject IJSRuntime JSRuntime
<p>
I'm the parent of Component2. I've got a message from my grand
child: @MessageService.Message
</p>
<Component2 />
<p>@jwtToken</p>
@code {
private string jwtToken;
protected override async Task OnInitializedAsync()
{
// await SetTokenAsync("my token");
jwtToken = await GetTokenAsync();
MessageService.Notify += OnNotify;
MessageService.Message = "A message from Index";
}
public void OnNotify()
{
InvokeAsync(() =>
{
StateHasChanged();
});
}
public void Dispose()
{
MessageService.Notify -= OnNotify;
}
public async Task<string> GetTokenAsync()
=> await JSRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");
public async Task SetTokenAsync(string token)
{
if (token == null)
{
await JSRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
}
else
{
await JSRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
}
}
}
注意,我们直接绑定到MessageService.Message属性,但是必须调用StateHasChanged方法来刷新文本的显示。
Component2.razor
<h3>Component2: I'm the parent of component three</h3>
<Component3/>
@code {
}
Component3.razor
@inject MessageService MessageService
<p>This is component3. Please type a message to my grand parent</p>
<input placeholder="Type a message to grandpa..." type="text"
@bind="@MessageService.Message" @bind:event="oninput" />
@code {
protected override void OnInitialized()
{
MessageService.Notify += () => InvokeAsync(() =>
{
StateHasChanged();
});
}
}
请注意,在 Component3 中,我们将 MessageService.Message 绑定到文本框,并且每次按下键盘时都会发生绑定(输入事件与更改 事件)。
就是这样,希望对您有所帮助,如有任何问题,请随时提出。