首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Blazor组件,它加载自己的脚本

Blazor组件,它加载自己的脚本
EN

Stack Overflow用户
提问于 2022-11-12 17:42:21
回答 2查看 81关注 0票数 0

我的自定义blazor组件使用外部脚本(托管在CDN上)。通常,人们会在index.html中引用它,但我不想这样做--我希望组件是独立的,易于使用。

典型的解决方案是脚本加载器。一个流行的实现已经浮动了多年:这里这里

wwwroot/js/scriptLoader.js (在index.html中引用):

代码语言:javascript
运行
复制
let _loadedScripts = [];      // list of loading / loaded scripts

export async function loadScript(src) {

  // only load script once
  if (_loadedScripts[src])
    return Promise.resolve();

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

    // mark script as loading/loaded
    _loadedScripts[src] = true;

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

    tag.onerror = function() {
      console.error('Failed to load script.');
      reject(src);
    }

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

然后创建一个组件,在其中加载自定义脚本。

FooComponent.razor

代码语言:javascript
运行
复制
@inject IJSRuntime _js

// etc...

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  await base.OnAfterRenderAsync(firstRender);
  if (firstRender)
  {
    // load component's script (using script loader)
    await _js.InvokeVoidAsync("loadScript", "https://cdn.example.com/foo.min.js");
  }
  // invoke `doFoo()` from 'foo.min.js' script
  await _js.InvokeVoidAsync("doFoo", "hello world!");
}

这是可行的。我可以使用<FooComponent />,它将加载自己的脚本文件。

但是,如果多次使用该组件,则会遇到以下情况:

  • 组件的实例1
    • 尝试加载脚本
    • 脚本加载器加载脚本
    • 组件可以使用它。

  • 组件2+的实例
    • 它们与实例1同时加载!
    • 每个加载程序都试图加载脚本,但加载程序拒绝多次加载它。
    • 他们立即尝试使用脚本-但它仍然忙着加载!
    • 所以它们都失败了,应用程序崩溃了(异常等等)

在使用之前,我如何重构代码以确保脚本实际完成加载?

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2022-11-13 12:21:24

解决问题的关键是确保:

  1. 只有一次尝试加载脚本。
  2. 在加载之前,不会调用任何方法。

实现这一目标的一种方法是使用每个SPA会话的单个进程来管理和调用脚本。

下面是一个使用服务来管理流程的解决方案。它使用基于任务的异步进程,利用TaskCompletionSource来确保在尝试调用脚本方法之前加载脚本文件。

首先,这两个脚本(都在wwwroot/js/中):

代码语言:javascript
运行
复制
// Your scriptLoader.js
// registered in _layout.html
let _loadedScripts = [];      // list of loading / loaded scripts

window.blazr_loadScript = async function(src) {

    // only load script once
    if (_loadedScripts[src])
        return Promise.resolve();

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

        // mark script as loading/loaded
        _loadedScripts[src] = true;

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

        tag.onerror = function () {
            console.error('Failed to load script.');
            reject(src);
        }

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

并在需要脚本时加载CDN。

代码语言:javascript
运行
复制
// special.js
window.blazr_Alert = function (message) {
    window.alert(message);
} 

接下来是一个作用域服务类,用于管理脚本调用进程。让它像你需要的那样通用。

代码语言:javascript
运行
复制
public class MyScriptLoadedService
{
    private IJSRuntime _js;
    private TaskCompletionSource? _taskCompletionSource;

    private Task LoadTask => _taskCompletionSource?.Task ?? Task.CompletedTask;

    public MyScriptLoadedService(IJSRuntime jSRuntime)
        => _js = jSRuntime;

    public async Task SayHello(string message)
    {
        // If this is the first time then _taskCompletionSource will be null
        // We need to create one and load the Script file
        if (_taskCompletionSource is null)
        {
            // Create a new TaskCompletionSource
            _taskCompletionSource = new TaskCompletionSource();
            // wait on loading the script
            await _js.InvokeVoidAsync("blazr_loadScript", "/js/special.js");
            // set the TaskCompletionSource to successfully completed.
            _taskCompletionSource.TrySetResult();
        }

        // if we get here then _taskCompletionSource has a valid Task which we can await.
        // If it's completed then we just hammer on through
        // If not we wait until it is 
        await LoadTask;

        // At this point we have a loaded script file so can safely call the script
        await _js.InvokeVoidAsync("blazr_Alert", message);
    }
}

创建一个扩展方法,以便在单独的库中轻松加载:

代码语言:javascript
运行
复制
public static class MyLibaryServiceCollectionExtensions
{
    public static void AddMyServices(this IServiceCollection services)
    {
        services.AddScoped<MyScriptLoadedService>();
    }
}

程序:

代码语言:javascript
运行
复制
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddMyServices();
builder.Services.AddSingleton<WeatherForecastService>();

下面是一个简单的测试组件MyAlert

代码语言:javascript
运行
复制
@inject MyScriptLoadedService Service

<h3>MyAlert for @this.Message</h3>

@code {
    [Parameter] public string Message { get; set; } = "Bonjour Blazor";

    protected override Task OnAfterRenderAsync(bool firstRender)
        => Service.SayHello(this.Message);
}

还有我的Index

代码语言:javascript
运行
复制
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<MyAlert Message="Brilliant England at the MCG" />
<MyAlert Message="Hello from France" />
<MyAlert Message="Hello from Germany" />
<MyAlert Message="Hello from Spain" />
票数 1
EN

Stack Overflow用户

发布于 2022-11-14 02:32:02

原来这是javascript的问题。下面的代码适用于多个并发组件。

wwwroot/js/scriptLoader.js (在index.html中引用):

代码语言:javascript
运行
复制
let _loadingAndLoadedScripts = [];


export async function loadJs(src) {
  let hash = generateHash(src);

  // scenario 1: first invocation, so load script
  if (!_loadingAndLoadedScripts[hash]) {
    return new Promise((resolve, reject) => {
      let tag  = document.createElement('script');
      tag.type = 'text/javascript';
      tag.id   = hash;
      tag.src  = src;

      tag.onload = () => {
        tag.setAttribute('data-loaded', true);
        document.dispatchEvent(new Event('scriptLoaded'));
        resolve();
      };

      tag.onerror = () => {
        console.error('Failed to load script \'' + src + '\'.');
        reject();
      }

      _loadingAndLoadedScripts[src] = true;   // save state
      document.body.appendChild(tag);
    });
  }

  // scenario 2: script is busy loading, or already loaded
  else {
    // if loaded, do nothing
    var script   = document.getElementById(hash);
    let isLoaded = script && script.getAttribute('data-loaded') === 'true';
    if (isLoaded) {
      return Promise.resolve();
    }
    // else loading, so wait
    else {
      return new Promise((resolve, reject) => {
        // room for improvement here: could start timer to timeout
        // after few seconds then reject; I didn't do that because
        // it's probably unnecessary
        document.addEventListener('scriptLoaded', (e) => {
          resolve();
        }, { 'once': true });
      });
    }
  }

}


// fast simple hasher, from https://stackoverflow.com/a/8831937/9971404
function generateHash(s) {
  let hash = 0;
  for (let i = 0, len = s.length; i < len; i++) {
    let c = s.charCodeAt(i);
    hash = (hash << 5) - hash + c;
    hash |= 0;
  }
  return hash;
}

出现原始代码中的争用条件是因为组件实例2+将尝试在加载脚本之前使用它。

这也意味着我所链接到的代码--在SO和博客和教程中大量引用--都存在严重缺陷。

在这个固定的代码中:

  • 它是幂等的:对于同一个src,可以安全地多次调用它
  • 组件实例1:一旦脚本加载,加载程序将在脚本标记上设置data-loaded属性,并分发componentLoaded事件
  • 组件实例2+:如果存在带有属性的脚本标记,则脚本将“加载”,因此返回到blazor,否则脚本将“加载”,因此请侦听componentLoaded事件(仅一次),然后返回blazor。
  • 脚本标记的id设置为其src的散列,这允许组件实例2+搜索标记
  • 从脚本加载器返回后,blazor代码可以确定脚本已经加载,因此不需要进一步的操作。

只有改进的余地(请参阅上面的代码注释)是将事件侦听器封装在一个定时器中,然后在几秒钟后超时,然后是reject()。我会在c#中这样做,但是js是单线程的,所以我怀疑是否会出现类似的争用状态,所以不要认为它是必要的。

对于组件的许多并发实例,这是非常完美的:

代码语言:javascript
运行
复制
<MyComponent Foo="spam" />
<MyComponent Foo="ham" />
<MyComponent Bar="1" />
<MyComponent Bar="2" />
<MyComponent Baz="a" />
<MyComponent Baz="b" />

组件是独立的,所以不需要引用脚本或类似的东西。这使得将组件集中在单独的库中非常方便--只需参考nuget并使用这些组件.不需要额外的努力!

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/74415181

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档