ASP.NET Core Blazor JavaScript と静的サーバー側レンダリング (静的 SSR)

この記事では、静的サーバー側レンダリング (静的 SSR) と拡張ナビゲーションを使用する Blazor Web アプリで JavaScript (JS) を読み込む方法について説明します。

一部のアプリは、JS が各ページに固有の初期化タスクの実行に依存する場合があります。 Blazor の拡張ナビゲーション機能はページ全体の再読み込みを可能にするものですが、これを使用するとき、JS が再実行されない可能性があります (拡張ページ ナビゲーションの際は通常は再実行されます)。

この問題を回避するため、コンポーネントに適用されているレイアウト ファイルの外に置かれているページ固有の <script> 要素に依存することはお勧めしません。 代わりに、スクリプトは初期化ロジックを実行する afterWebStartedJS初期化子 を登録し、イベント リスナー (blazor.addEventListener("enhancedload", callback)) を使用して、拡張ナビゲーションによって引き起こされるページの更新をリッスンする必要があります。

次の例は、拡張ナビゲーションのある静的にレンダリングされたページが最初に読み込まれたか、更新されたときに実行する JS コードを構成する 1 つの方法を示しています。

次の PageWithScript コンポーネントの例は、静的 SSR と拡張ナビゲーションを使ってスクリプトを実行する必要があるアプリ内のコンポーネントです。 次のコンポーネントの例には、この記事の後半でソリューションに追加される Razor クラス ライブラリ (RCL) の PageScript コンポーネントが含まれています。

Components/Pages/PageWithScript.razor:

@page "/page-with-script"
@using BlazorPageScript

<PageTitle>Enhanced Load Script Example</PageTitle>

<PageScript Src="./Components/Pages/PageWithScript.razor.js" />

Welcome to my page.

Blazor Web アプリで、次の併置された JS ファイルを追加します。

  • onLoad は、スクリプトがページに追加されたときに呼び出されます。
  • onUpdate は、強化された更新プログラムの後にスクリプトがページにまだ存在する場合に呼び出されます。
  • onDispose は、強化された更新後にスクリプトがページから削除されたときに呼び出されます。

Components/Pages/PageWithScript.razor.js:

export function onLoad() {
  console.log('Loaded');
}

export function onUpdate() {
  console.log('Updated');
}

export function onDispose() {
  console.log('Disposed');
}

Razor クラス ライブラリ (RCL) (RCL の例では BlazorPageScriptという名前) で、次のモジュールを追加します。

wwwroot/BlazorPageScript.lib.module.js:

const pageScriptInfoBySrc = new Map();

function registerPageScriptElement(src) {
  if (!src) {
    throw new Error('Must provide a non-empty value for the "src" attribute.');
  }

  let pageScriptInfo = pageScriptInfoBySrc.get(src);

  if (pageScriptInfo) {
    pageScriptInfo.referenceCount++;
  } else {
    pageScriptInfo = { referenceCount: 1, module: null };
    pageScriptInfoBySrc.set(src, pageScriptInfo);
    initializePageScriptModule(src, pageScriptInfo);
  }
}

function unregisterPageScriptElement(src) {
  if (!src) {
    return;
  }

  const pageScriptInfo = pageScriptInfoBySrc.get(src);
  
  if (!pageScriptInfo) {
    return;
  }

  pageScriptInfo.referenceCount--;
}

async function initializePageScriptModule(src, pageScriptInfo) {
  if (src.startsWith("./")) {
    src = new URL(src.substr(2), document.baseURI).toString();
  }

  const module = await import(src);

  if (pageScriptInfo.referenceCount <= 0) {
    return;
  }

  pageScriptInfo.module = module;
  module.onLoad?.();
  module.onUpdate?.();
}

function onEnhancedLoad() {
  for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
    if (referenceCount <= 0) {
      module?.onDispose?.();
      pageScriptInfoBySrc.delete(src);
    }
  }

  for (const { module } of pageScriptInfoBySrc.values()) {
    module?.onUpdate?.();
  }
}

export function afterWebStarted(blazor) {
  customElements.define('page-script', class extends HTMLElement {
    static observedAttributes = ['src'];

    attributeChangedCallback(name, oldValue, newValue) {
      if (name !== 'src') {
        return;
      }

      this.src = newValue;
      unregisterPageScriptElement(oldValue);
      registerPageScriptElement(newValue);
    }

    disconnectedCallback() {
      unregisterPageScriptElement(this.src);
    }
  });

  blazor.addEventListener('enhancedload', onEnhancedLoad);
}

次の PageScript コンポーネントを RCL に追加します。

PageScript.razor:

<page-script src="@Src"></page-script>

@code {
    [Parameter]
    [EditorRequired]
    public string Src { get; set; } = default!;
}

PageScript コンポーネントは通常、ページの最上位レベルで機能します。

PageScript コンポーネントをアプリのレイアウト (たとえば、MainLayout.razor) に配置すると、そのレイアウトを使うページ間で共有 PageScript になり、次に、コンポーネントによりページ全体のリロード後にのみ onLoad が実行され、拡張ナビゲーションなどの拡張ページの更新が発生したときに onUpdate が実行されます。

ページ間で同じモジュールを再利用し、各ページ変更で onLoadonDispose コールバックを呼び出すには、スクリプトの末尾にクエリ文字列を追加して、別のモジュールとして認識されるようにします。 アプリでは、コンポーネントの名前をクエリ文字列値として使用する規則を採用できます。 次の例では、この PageScript コンポーネント参照が Counter コンポーネントに配置されるため、クエリ文字列は "counter" です。 これは単なる提案であり、任意のクエリ文字列スキームを使用できます。

<PageScript Src="./Components/Pages/PageWithScript.razor.js?counter" />

特定の DOM 要素の変更を監視するには、クライアントの JS で MutationObserver パターンを使用します。 詳しくは、「ASP.NET Core Blazor JavaScript の相互運用性 (JS 相互運用)」をご覧ください。

RCL を使わない実装例

この記事で説明する方法は、Razor クラス ライブラリ (RCL) を使わずに、Blazor Web アプリに直接実装できます。 例については、「ASP.NET Core Blazor Web アプリで TOTP 認証アプリの QR コード生成を有効にする」を参照してください。