[英]Blazor component which loads its own script
My custom blazor component uses an external script (hosted on a CDN).我的自定义 blazor 组件使用外部脚本(托管在 CDN 上)。 Usually one would reference it in
index.html
, but I don't want to do that - I want the component to be self contained and easy to use.通常人们会在
index.html
中引用它,但我不想那样做——我希望该组件是独立的并且易于使用。
The typical solution is a script loader.典型的解决方案是脚本加载器。 A popular implementation has been floating around for years: here and here .
一个流行的实现已经流行了很多年:这里和 这里。
wwwroot/js/scriptLoader.js
(referenced in index.html
): wwwroot/js/scriptLoader.js
(在index.html
中引用):
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);
});
}
Then I create a component where I want to load a custom script.然后我创建了一个组件,我想在其中加载自定义脚本。
FooComponent.razor
: FooComponent.razor
:
@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!");
}
That works.这样可行。 I can use the
<FooComponent />
and it will load its own script file.我可以使用
<FooComponent />
它会加载自己的脚本文件。
But if I use the component multiple times, I run into a race condition:但是,如果我多次使用该组件,就会遇到竞争条件:
How can I refactor the code to ensure that the script is actually finished loading, before using it?在使用脚本之前,如何重构代码以确保脚本实际上已完成加载?
The key to your problem is to ensure:您的问题的关键是确保:
One way to achieve this is to use a single process per SPA session to manage and call the scripts.实现此目的的一种方法是使用每个 SPA session 的单个进程来管理和调用脚本。
Here's a solution that uses a service to manage the process.这是一个使用服务来管理流程的解决方案。 It uses a Task based async process utilizing
TaskCompletionSource
to ensure the script file is loaded before attempting to call a script method.它使用基于任务的异步进程,利用
TaskCompletionSource
来确保在尝试调用脚本方法之前加载脚本文件。
First the two scripts (both in wwwroot/js/):首先是两个脚本(都在 wwwroot/js/ 中):
// 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);
});
}
And the CDN load when needed script.并在需要脚本时加载CDN 。
// special.js
window.blazr_Alert = function (message) {
window.alert(message);
}
Next a Scoped Service class to manage the script calling process.接下来是一个 Scoped Service class 来管理脚本调用过程。 Make it as generic as you need to.
使其尽可能通用。
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);
}
}
Create an extension method to make loading easy if in a separate library:如果在单独的库中,请创建一个扩展方法以简化加载:
public static class MyLibaryServiceCollectionExtensions
{
public static void AddMyServices(this IServiceCollection services)
{
services.AddScoped<MyScriptLoadedService>();
}
}
Program:程序:
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddMyServices();
builder.Services.AddSingleton<WeatherForecastService>();
Here's a simple test component MyAlert
这是一个简单的测试组件
MyAlert
@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);
}
And my Index
:我的
Index
:
@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" />
Turns out this was javascript problem.原来这是 javascript 问题。 The code below works for multiple concurrent components.
下面的代码适用于多个并发组件。
wwwroot/js/scriptLoader.js
(referenced in index.html
): wwwroot/js/scriptLoader.js
(在index.html
中引用):
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;
}
The race condition in the original code occurred because component instances 2+ would try to use the script before it was loaded.原始代码中出现竞争条件是因为组件实例 2+ 会在加载脚本之前尝试使用该脚本。
It also means that the codes I linked to above - which are heavily referenced in SO and blogs and tutorials - are badly flawed.这也意味着我上面链接的代码——在 SO 和博客和教程中被大量引用——存在严重缺陷。
In this fixed code:在此固定代码中:
src
src
安全地调用多次data-loaded
attribute on the script tag, and dispatches the componentLoaded
eventdata-loaded
属性,并调度componentLoaded
事件componentLoaded
event (only once) then return to blazorcomponentLoaded
事件(仅一次),然后返回 blazorid
is set to a hash of its src
, which allows component instances 2+ to search for the tagid
设置为其src
的 hash,这允许组件实例 2+ 搜索标签Only room for improvement (see code comment above) is to wrap the event listener in a timer, and timeout after a few seconds then reject()
.唯一的改进空间(参见上面的代码注释)是将事件侦听器包装在计时器中,并在几秒后超时然后
reject()
。 I'd do that in c#, but js is single threaded, so I doubt that sort of race condition could occur, and so don't think it's necessary.我会在 c# 中这样做,但是 js 是单线程的,所以我怀疑会出现这种竞争条件,因此认为没有必要。
This works perfectly, for many concurrent instances of a component:对于组件的许多并发实例,这非常有效:
<MyComponent Foo="spam" />
<MyComponent Foo="ham" />
<MyComponent Bar="1" />
<MyComponent Bar="2" />
<MyComponent Baz="a" />
<MyComponent Baz="b" />
And the component is self-contained, so no need to reference scripts or anything like that.而且该组件是独立的,因此无需引用脚本或类似的东西。 This makes it very convenient for centralising components in a separate library - just reference the nuget and use the components... no extra effort required!
这使得将组件集中在一个单独的库中非常方便 - 只需引用 nuget 并使用这些组件......无需额外的努力!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.