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

解决Blazor组件自包含加载CDN外部脚本的竞态问题

解决Blazor组件多实例下的外部脚本加载竞态问题

问题场景

开发自定义Blazor组件时,需要依赖托管在CDN的外部脚本,为了保持组件自包含,不在index.html中提前引用该脚本。使用了常见的脚本加载器实现,但当页面中多次使用该组件时,出现了竞态问题:

  • 第一个组件实例触发脚本加载后,加载器标记该脚本为"已加载/加载中"
  • 后续组件实例调用加载器时,直接得到已完成的Promise,随即调用脚本中的方法,但此时脚本实际还在加载过程中,导致应用崩溃抛出异常

原脚本加载器代码:

let _loadedScripts = [];      // 记录加载中/已加载的脚本

export async function loadScript(src) {

  // 只加载一次脚本
  if (_loadedScripts[src])
    return Promise.resolve();

  return new Promise(function(resolve, reject) {
    let tag  = document.createElement('script');
    tag.type = 'text/javascript';
    tag.src  = src;

    // 标记脚本为加载中/已加载
    _loadedScripts[src] = true;

    tag.onload = function() {
      resolve();
    }

    tag.onerror = function() {
      console.error('脚本加载失败。');
      reject(src);
    }

    document.body.appendChild(tag);
  });
}

原Blazor组件调用代码:

@inject IJSRuntime _js

// 其他代码...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  await base.OnAfterRenderAsync(firstRender);
  if (firstRender)
  {
    // 通过加载器加载组件依赖脚本
    await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  }
  // 调用脚本中的doFoo方法
  await _js.InvokeVoidAsync("doFoo", "hello world!");
}

问题根源

原加载器中,_loadedScripts[src]被设置为true的时机是在创建script标签之后、脚本实际加载完成之前。这导致后续组件调用loadScript时,误以为脚本已经加载完成,直接返回resolved的Promise,进而提前调用了脚本中的方法,此时脚本尚未加载完成,自然会抛出异常。

解决方案

1. 重构脚本加载器

修改_loadedScripts的存储逻辑,不再存储布尔值,而是存储脚本加载的Promise本身。这样所有组件实例都会等待同一个Promise完成,确保脚本真正加载完成后再执行后续逻辑:

let _loadedScripts = {};      // 存储脚本对应的加载Promise

export async function loadScript(src) {
  // 如果已有加载中的Promise,直接返回它
  if (_loadedScripts[src]) {
    return _loadedScripts[src];
  }

  const loadPromise = new Promise(function(resolve, reject) {
    let tag  = document.createElement('script');
    tag.type = 'text/javascript';
    tag.src  = src;

    tag.onload = function() {
      resolve();
    }

    tag.onerror = function() {
      console.error('脚本加载失败。');
      // 加载失败时移除缓存的Promise,允许后续重试
      delete _loadedScripts[src];
      reject(src);
    }

    document.body.appendChild(tag);
  });

  // 将Promise存入缓存
  _loadedScripts[src] = loadPromise;
  return loadPromise;
}

2. 调整Blazor组件调用逻辑

确保每次调用脚本方法前,都等待loadScript完成,而不是仅在首次渲染时加载脚本。这样即使组件多次渲染,也能保证脚本就绪后再执行方法:

@inject IJSRuntime _js

// 其他代码...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  await base.OnAfterRenderAsync(firstRender);
  
  // 确保脚本加载完成后再调用方法
  await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  await _js.InvokeVoidAsync("doFoo", "hello world!");
}

如果希望仅在首次渲染时触发加载(后续复用已加载的脚本),也可以保留firstRender判断,但必须确保方法调用在loadScript之后:

@inject IJSRuntime _js

// 其他代码...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  await base.OnAfterRenderAsync(firstRender);
  
  if (firstRender)
  {
    // 首次渲染时加载脚本
    await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  }
  else
  {
    // 非首次渲染时,仍需等待脚本加载完成(处理竞态场景)
    await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  }
  
  await _js.InvokeVoidAsync("doFoo", "hello world!");
}

或者更简洁的写法,不管是否首次渲染,都调用loadScript——因为加载器已经做了缓存,重复调用不会重复加载脚本,只会等待已有的Promise完成:

@inject IJSRuntime _js

// 其他代码...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  await base.OnAfterRenderAsync(firstRender);
  
  // 不管是否首次渲染,都等待脚本加载完成
  await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  await _js.InvokeVoidAsync("doFoo", "hello world!");
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 08:10:47