mcpField
U
use-js-interop

use-js-interop

github.com/dotnet開発5.8k3.1k 回インストール1 日前に更新
リポジトリ

use-js-interop について

Blazor 開発者向けに、JavaScript 相互運用の呼び出し時期、モジュールのライフサイクル、バッチ処理、コールバックの安全性をまとめたスキルです。コンポーネント隣接モジュールと型付きラッパーで、.NET と JavaScript 間の呼び出しを管理します。

主な機能

  1. 01隣接する .razor.js モジュールの使用
  2. 02レンダリング後またはイベント内での JS 呼び出し
  3. 03関連する相互運用呼び出しの一括化
  4. 04型付きクラスによるモジュールライフサイクルの管理
  5. 05コールバック、切断、非同期クリーンアップへの対応

活用シーン

  1. 01Blazor コンポーネントからブラウザー側 JavaScript を利用
  2. 02Blazor Server の SignalR 相互運用往復の削減
  3. 03JS コールバック、コンポーネント状態更新、後片付けの管理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dotnet/skills --skill use-js-interop

インストール後、Claude Code で/use-js-interopを入力して読み込まれたか確認します。

詳細

Blazor での JS 相互運用

1. 同じ場所に配置する JS モジュール

export を使う同じ場所に配置した .razor.js ファイルを必ず使用してください。グローバルな window.* 関数や <script> タグは使用しないでください。

// ChartPanel.razor.js — placed next to ChartPanel.razor
export function initialize(canvas, dotNetRef) { /* ... */ }
export function updateData(points) { /* ... */ }
export function dispose() { /* ... */ }

インポートパス:同じプロジェクトの場合は "./Components/ChartPanel.razor.js"、RCL の場合は "./_content/{AssemblyName}/..." です。

2. ライフサイクルのタイミング

すべての JS 相互運用は OnAfterRenderAsync またはイベントハンドラー内で行う必要があります。OnInitialized、OnParametersSet、コンストラクター内では行わないでください。サーバーの事前レンダリング中は JS を使用できません。

型付きの相互運用ラッパーを使用してください(セクション 4 を参照)。生の文字列リテラルを使って InvokeAsync/InvokeVoidAsync を呼び出さないでください。

private ChartInterop? _chart;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _chart = new ChartInterop(JS);
        await _chart.InitializeAsync(_canvasRef);
    }
}

パラメーターの変更:OnParametersSet でフラグを設定し、OnAfterRenderAsync で反映します。

private bool _dataChanged;

protected override void OnParametersSet() => _dataChanged = true;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender) { /* init */ }
    else if (_dataChanged && _chart is not null)
    {
        _dataChanged = false;
        await _chart.UpdateDataAsync(DataPoints);
    }
}

3. 関連する操作をまとめる

JS 相互運用の呼び出しごとに、.NET と JS の境界(Blazor Server では SignalR 回線も)をまたぎます。バッチ処理は両方向、つまり .NET→JS と JS→.NET のどちらにも適用されます。

.NET → JS:連続する呼び出しをまとめる

C# 側で JS 呼び出しを 2 回以上連続して行う場合は、1 つの JS 関数にまとめます。

// ❌ Two round-trips — theme and locale are always applied together
await _module.InvokeVoidAsync("applyTheme", theme);
await _module.InvokeVoidAsync("applyLocale", locale);

// ❌ Result of one call feeds into another — both can stay in JS
var token = await _module.InvokeAsync<string>("createAccessToken");
await _module.InvokeVoidAsync("storeToken", token);
// ✅ One call applies both — no data dependency, no reason for two trips
export function applyPreferences(theme, locale) {
    document.documentElement.dataset.theme = theme;
    document.documentElement.lang = locale;
}

// ✅ Chain stays in JS — the token never needs to cross the boundary
export function createAndStoreToken() {
    const token = crypto.randomUUID();
    sessionStorage.setItem('access-token', token);
    return token;
}
JS → .NET:コールバックをまとめる

JS から複数のデータを .NET に返す場合は、コールバックを個別に実行するのではなく、1 回の invokeMethodAsync 呼び出しでまとめて送信します。

// ❌ Two .NET round-trips from JS
await dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);
await dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);

// ✅ One callback with all data
await dotNetRef.invokeMethodAsync(ON_PLAYER_STATE_CHANGED, { volume, isPlaying });

ルール:どちらかの側で 2 つの相互運用呼び出しが常に一緒に行われる場合は、1 つの関数にまとめてください。

4. 型付き相互運用ラッパー

機能ごとの相互運用処理を、モジュールのライフサイクルを管理するプレーンなクラスにカプセル化します。

public sealed class ChartInterop : IAsyncDisposable
{
    internal const string ModulePath = "./Components/ChartPanel.razor.js";
    internal const string InitMethod = "initialize";
    internal const string UpdateMethod = "updateData";
    internal const string DisposeMethod = "dispose";

    private readonly IJSRuntime _js;
    private IJSObjectReference? _module;

    public ChartInterop(IJSRuntime js) => _js = js;

    private async ValueTask<IJSObjectReference> GetModuleAsync()
        => _module ??= await _js.InvokeAsync<IJSObjectReference>("import", ModulePath);

    public async ValueTask InitializeAsync(ElementReference canvas)
    {
        var module = await GetModuleAsync();
        await module.InvokeVoidAsync(InitMethod, canvas);
    }

    public async ValueTask UpdateDataAsync(IReadOnlyList<DataPoint> points)
    {
        var module = await GetModuleAsync();
        await module.InvokeVoidAsync(UpdateMethod, points);
    }

    public async ValueTask DisposeAsync()
    {
        try
        {
            if (_module is not null)
            {
                await _module.InvokeVoidAsync(DisposeMethod);
                await _module.DisposeAsync();
            }
        }
        catch (JSDisconnectedException) { }
    }
}

コンポーネントは、マジック文字列を使わずにラッパーを作成して使用します。

@inject IJSRuntime JS
@implements IAsyncDisposable

<canvas @ref="_canvasRef" width="600" height="400"></canvas>

@code {
    private ElementReference _canvasRef;
    private ChartInterop? _chart;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _chart = new ChartInterop(JS);
            await _chart.InitializeAsync(_canvasRef);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (_chart is not null)
            await _chart.DisposeAsync();
    }
}

相互運用ラッパーには、インターフェイスと実装の組み合わせより、具象クラスを使用してください。単体テストでは、IJSRuntime を直接差し替えます(これはすでにインターフェイスです)。

5. JS から .NET へのコールバックに使用する DotNetObjectReference

_dotNetRef = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("initialize", _dotNetRef);

JS 側では、dotNetRef をクラスでラップします。回線切断に備えて、.catch() ではなく try/catch と async/await を使用します。.NET メソッド名の定数は先頭で定義します。

const ON_CLIPBOARD_CHANGED = 'OnClipboardChanged';

class ClipboardMonitor {
    #dotNetRef;
    #abortController;

    constructor(dotNetRef) {
        this.#dotNetRef = dotNetRef;
        this.#abortController = new AbortController();
    }

    start() {
        document.addEventListener('copy', async () => {
            try {
                const text = await navigator.clipboard.readText();
                await this.#dotNetRef.invokeMethodAsync(ON_CLIPBOARD_CHANGED, text);
            } catch { /* circuit disconnected or clipboard denied */ }
        }, { signal: this.#abortController.signal });
    }

    dispose() {
        this.#abortController.abort();
    }
}

let monitor;
export function initialize(dotNetRef) {
    monitor = new ClipboardMonitor(dotNetRef);
    monitor.start();
}

export function dispose() {
    monitor?.dispose();
}

ルール:

  • [JSInvokable] メソッドは**public である必要があります**。private/internal にすると、実行時にエラーなく失敗します
  • [JSInvokable] コールバック内では、StateHasChanged を InvokeAsync でラップします。
    [JSInvokable]
    public async Task OnClipboardChanged(string text)
    {
        await InvokeAsync(() => { _lastClipboard = text; StateHasChanged(); });
    }
    
  • JS の invokeMethodAsync の呼び出しは、必ず try/catch で囲みます。回線が切断されると例外が発生します
  • JS の .NET メソッド名文字列には const を使用します。これにより、気づかれないまま失敗するタイプミスを防げます
  • DisposeAsync で DotNetObjectReference を破棄します

6. 破棄とサーバーの安全性

必ず IAsyncDisposable を実装します。最初に JS のクリーンアップを呼び出し、その後で参照を破棄します。Blazor Server の回線切断に備えて、JSDisconnectedException をキャッチします。

public async ValueTask DisposeAsync()
{
    try
    {
        if (_module is not null)
        {
            await _module.InvokeVoidAsync("dispose");
            await _module.DisposeAsync();
        }
    }
    catch (JSDisconnectedException) { }

    _dotNetRef?.Dispose();
}

JS 相互運用のクリーンアップに同期的な IDisposable は使用しないでください。InvokeVoidAsync は ValueTask を返すため、await する必要があります。

7. ElementReference

文字列 ID ではなく、@ref で DOM 要素を渡します。

<canvas @ref="_canvasRef" width="600" height="400"></canvas>
await _chart.InitializeAsync(_canvasRef);

チェックリスト

  • JS は export を使う同じ場所に配置した .razor.js に記述し、window.* グローバルは使用しない
  • すべての相互運用は OnAfterRenderAsync またはイベントハンドラー内で行い、事前レンダリング中は実行しない
  • IAsyncDisposable で JSDisconnectedException をキャッチする
  • DotNetObjectReference は DisposeAsync で破棄し、JS 側では invokeMethodAsync を try/catch で囲む
  • [JSInvokable] メソッドは public にし、await InvokeAsync(StateHasChanged) を使用する
  • 戻り値が不要な場合は InvokeVoidAsync を使用する
  • 文字列 ID の代わりに ElementReference を使用する
  • 関連する操作は 1 回の相互運用呼び出しにまとめる(.NET→JS と JS→.NET の両方)

よくある間違いのチェックリスト

間違い修正方法
CSSで実現できることにJSを使うCSSカスタムプロパティ、data-属性、疑似クラスを使用します
細分化された相互運用呼び出しが多い.NET→JS、JS→.NETの両方で、まとめて粗粒度の関数にします
コンポーネントがJSモジュールを直接インポートする型安全な相互運用クラスにカプセル化します
メソッド名/モジュールパスにマジック文字列を使う相互運用クラス内でinternal constフィールドを定義します
相互運用ラッパーにインターフェイスと実装を用意する通常のクラスを使い、代わりにテストではIJSRuntimeをモックします
OnInitializedAsync内でJSを呼び出すOnAfterRenderAsync(firstRender)に移します
戻り値のない呼び出しにInvokeAsync<object>を使うInvokeVoidAsyncを使用します
JSを呼び出さずに破棄するIDisposableawaitを使うIAsyncDisposableを使用します
グローバルなwindow.* JS関数exportを使う、コンポーネントと同じ場所に配置した.razor.jsを使用します
文字列の要素IDをJSに渡す@refでElementReferenceを使用します
プライベートメソッドに[JSInvokable]を付けるpublicである必要があります。そうでない場合、警告なく失敗します
DotNetObjectReferenceを破棄しないDisposeAsyncで破棄します。破棄しないとメモリリークが発生します
InvokeAsyncを使わずにStateHasChanged()を呼び出すawait InvokeAsync(() => { StateHasChanged(); })で囲みます
エラー処理なしでJSのinvokeMethodAsyncを呼び出すtry/catchで囲みます。回線の切断時に例外が発生します
JSのイベントハンドラーでdotNetRefをそのまま使う#dotNetRefプライベートフィールドを持つクラスで囲みます
JSのinvokeMethodAsync呼び出しでマジック文字列を使うモジュールの先頭でconstを使います。誤記があっても実行時に警告なく失敗します
OnParametersSetAsync内でJSを呼び出す変更を追跡し、ガードを設けてOnAfterRenderAsyncで適用します
モジュールを呼び出す前にnullチェックを行わない使用前にmodule is not nullを確認します

コメント