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

Blazor预渲染下JS互调用异常及事件驱动组件优化求助

Blazor预渲染期间JS互操作调用间歇性异常的优化方案

问题描述

控制台间歇性抛出如下异常:

未捕获(在 promise 中)错误: System.InvalidOperationException: 此时无法发起 JavaScript 互操作调用。这是因为组件正在进行静态渲染。当启用预渲染时,JavaScript 互操作调用只能在 OnAfterRenderAsync 生命周期方法中执行。位于 Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, CancellationToken cancellationToken, Object[] args) 位于 Microsoft.JSInterop.JSRuntime.InvokeAsync[TValue](Int64 targetInstanceId, String identifier, Object[] args) 位于

我理解错误原因:事件驱动组件由DOM/客户端通过JavaScript触发更新,JS触发代码如下:

DotNet.invokeMethodAsync('onlymusik.com.Web', 'OnVideoEnded').then(data => { console.log('OnVideoEnded, return: ' + data); });

推测是事件处理程序在OnAfterRenderAsync()完成前触发导致的。组件中的事件处理代码如下:

private static AudioPlayer? _instance;

[JSInvokableAttribute("OnVideoEnded")]
public static async Task<string> VideoEnded()
{
    if (_instance != null)
    {
        await _instance.HandleVideoEnded();
        return "Success: ";
    }
    return "No active AudioPlayer instance";
}

private async Task HandleVideoEnded()
{
    // Prevent concurrent executions
    await InvokeAsync(async () =>
    {
        await jsRuntime.InvokeVoidAsync("playerLog",$"HandleVideoEnded() InvokeAsync _isHandlingVideoEnded: {_isHandlingVideoEnded}");
    });

    if (_isHandlingVideoEnded) return;

    try
    {
        _isHandlingVideoEnded = true;

        // Only use JS interop if we're not prerendering
        if (!_isPrerendering)
        {
            await InvokeAsync(async () =>
            {
                isPlaying = false;
                hasPlayed = true;
                try
                {
                    await jsRuntime.InvokeVoidAsync("playerLog", "HandleVideoEnded()");
                    await jsRuntime.InvokeVoidAsync("playerLog", "Preparing to play next track");
                    await PlayNextTrack();
                    await jsRuntime.InvokeVoidAsync("playerLog", "OnVideoEndedCallback.InvokeAsync");
                    await OnVideoEndedCallback.InvokeAsync(null);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error in HandleVideoEnded JS operations");
                }
            });
        }
        else
        {
            _logger.LogInformation("Skipping JS interop during prerendering in HandleVideoEnded");
        }
    }
    finally
    {
        _isHandlingVideoEnded = false;
    }
}

不想编写大量事件排队重放的冗余代码,我的代码有没有明显不符合Blazor事件驱动架构的问题?或者有什么优化建议?


优化建议

1. 延迟JS事件注册,从根源避免预渲染调用

预渲染期间组件还未完成初始化,直接让JS触发DotNet方法必然报错。正确做法是在组件首次渲染完成后,再通知JS注册事件:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _isPrerendering = false;
        // 通知JS注册视频结束事件监听
        await jsRuntime.InvokeVoidAsync("registerVideoEndedListener");
    }
    await base.OnAfterRenderAsync(firstRender);
}

同时修改JS代码,移除页面加载时的自动绑定,改为等待Blazor通知后再注册:

window.registerVideoEndedListener = () => {
    const video = document.getElementById('your-video-element');
    if (video) {
        video.addEventListener('ended', () => {
            DotNet.invokeMethodAsync('onlymusik.com.Web', 'OnVideoEnded')
                .then(data => console.log('OnVideoEnded, return: ' + data));
        });
    }
};

2. 简化InvokeAsync嵌套,避免上下文混乱

当前代码中多次嵌套InvokeAsync完全没必要——[JSInvokable]方法本身已经运行在Blazor的同步上下文内,除非涉及UI更新的操作,否则无需额外包裹。简化后的HandleVideoEnded:

private async Task HandleVideoEnded()
{
    if (_isHandlingVideoEnded) return;

    try
    {
        _isHandlingVideoEnded = true;

        if (!_isPrerendering)
        {
            isPlaying = false;
            hasPlayed = true;
            try
            {
                await jsRuntime.InvokeVoidAsync("playerLog", "HandleVideoEnded()");
                await jsRuntime.InvokeVoidAsync("playerLog", "Preparing to play next track");
                await PlayNextTrack();
                await jsRuntime.InvokeVoidAsync("playerLog", "OnVideoEndedCallback.InvokeAsync");
                await OnVideoEndedCallback.InvokeAsync(null);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in HandleVideoEnded JS operations");
            }
        }
        else
        {
            _logger.LogInformation("Skipping JS interop during prerendering in HandleVideoEnded");
        }
    }
    finally
    {
        _isHandlingVideoEnded = false;
    }
}

3. 修复静态实例_instance的多实例冲突风险

用静态字段存储组件实例,在多用户或多组件实例场景下会出现覆盖问题。建议在组件销毁时清空实例:

public void Dispose()
{
    if (_instance == this)
    {
        _instance = null;
    }
    // 取消JS侧的事件监听,避免内存泄漏
    jsRuntime.InvokeVoidAsync("unregisterVideoEndedListener");
}

4. 预渲染期间的事件兜底判断

如果确实存在极端场景无法避免预渲染触发事件,可以在静态方法中直接判断组件渲染状态,提前拦截:

[JSInvokableAttribute("OnVideoEnded")]
public static async Task<string> VideoEnded()
{
    if (_instance != null && !_instance._isPrerendering)
    {
        await _instance.HandleVideoEnded();
        return "Success: ";
    }
    return _instance == null ? "No active AudioPlayer instance" : "Skipped during prerendering";
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 10:17:08