[英]Blazor component which loads its own script
我的自定義 blazor 組件使用外部腳本(托管在 CDN 上)。 通常人們會在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);
});
}
然后我創建了一個組件,我想在其中加載自定義腳本。
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!");
}
這樣可行。 我可以使用<FooComponent />
它會加載自己的腳本文件。
但是,如果我多次使用該組件,就會遇到競爭條件:
在使用腳本之前,如何重構代碼以確保腳本實際上已完成加載?
您的問題的關鍵是確保:
實現此目的的一種方法是使用每個 SPA session 的單個進程來管理和調用腳本。
這是一個使用服務來管理流程的解決方案。 它使用基於任務的異步進程,利用TaskCompletionSource
來確保在嘗試調用腳本方法之前加載腳本文件。
首先是兩個腳本(都在 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);
});
}
並在需要腳本時加載CDN 。
// special.js
window.blazr_Alert = function (message) {
window.alert(message);
}
接下來是一個 Scoped Service class 來管理腳本調用過程。 使其盡可能通用。
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);
}
}
如果在單獨的庫中,請創建一個擴展方法以簡化加載:
public static class MyLibaryServiceCollectionExtensions
{
public static void AddMyServices(this IServiceCollection services)
{
services.AddScoped<MyScriptLoadedService>();
}
}
程序:
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddMyServices();
builder.Services.AddSingleton<WeatherForecastService>();
這是一個簡單的測試組件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);
}
我的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" />
原來這是 javascript 問題。 下面的代碼適用於多個並發組件。
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;
}
原始代碼中出現競爭條件是因為組件實例 2+ 會在加載腳本之前嘗試使用該腳本。
這也意味着我上面鏈接的代碼——在 SO 和博客和教程中被大量引用——存在嚴重缺陷。
在此固定代碼中:
src
安全地調用多次data-loaded
屬性,並調度componentLoaded
事件componentLoaded
事件(僅一次),然后返回 blazorid
設置為其src
的 hash,這允許組件實例 2+ 搜索標簽唯一的改進空間(參見上面的代碼注釋)是將事件偵聽器包裝在計時器中,並在幾秒后超時然后reject()
。 我會在 c# 中這樣做,但是 js 是單線程的,所以我懷疑會出現這種競爭條件,因此認為沒有必要。
對於組件的許多並發實例,這非常有效:
<MyComponent Foo="spam" />
<MyComponent Foo="ham" />
<MyComponent Bar="1" />
<MyComponent Bar="2" />
<MyComponent Baz="a" />
<MyComponent Baz="b" />
而且該組件是獨立的,因此無需引用腳本或類似的東西。 這使得將組件集中在一個單獨的庫中非常方便 - 只需引用 nuget 並使用這些組件......無需額外的努力!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.