📥

Blazor で JavaScript 相互運用を使ってファイルをダウンロードする

に公開

はじめに

Blazor アプリでファイルをダウンロードさせたいケースがあります。ブラウザでファイルをダウンロードさせるには JavaScript の力を借りる必要がありますが、Blazor には JavaScript 相互運用 (JS interop) 機能があるので、これを使って実現できます。

この記事では、MS Learn の公式ドキュメントで紹介されている推奨パターンを使ってファイルのダウンロードを実装する方法を紹介します。

URL からファイルをダウンロードする

Blob の URL など、ダウンロード対象のファイルの URL が分かっている場合のパターンです。公式ドキュメントでは triggerFileDownload という JavaScript 関数を定義して、C# 側から呼び出す方法が紹介されています。

JavaScript 関数を定義する

まず、ダウンロードを実行する JavaScript 関数を定義します。この関数は <a> 要素を動的に作成してクリックイベントを発火させることで、ブラウザのダウンロード機能を利用しています。

window.triggerFileDownload = (fileName, url) => {
    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = fileName ?? '';
    anchorElement.click();
    anchorElement.remove();
}

C# 側から呼び出す

Blazor コンポーネントから IJSRuntime.InvokeVoidAsync でこの関数を呼び出します。ファイル名と URL を引数として渡します。

@inject IJSRuntime JS

<button @onclick="DownloadFile">ダウンロード</button>

@code {
    private async Task DownloadFile()
    {
        var fileName = attachment.FileName;
        var fileUrl = attachment.BlobUrl;
        await JS.InvokeVoidAsync("triggerFileDownload", fileName, fileUrl);
    }
}

ポイントは、ファイル名や URL を JavaScript 関数の引数として渡している ところです。こうすることで値は JavaScript の文字列として安全に扱われます。
JavaScript 関数の引数として渡さずに、eval 関数などを C# から呼び出して直接ファイル名などを含んだ JavaScript 渡すとファイル名などに悪意あるユーザーからの入力が渡されると XSS などの問題が起きる可能性があるので、めんどくさくても必ず JavaScript 関数を定義してから呼び出すようにしましょう。

JavaScript モジュール分離でよりクリーンに

上の例では window オブジェクトにグローバル関数を定義していますが、Blazor では JavaScript モジュール分離が推奨されています。JS モジュールを使うと以下のメリットがあります。

  • グローバル名前空間を汚染しない
  • ライブラリやコンポーネントの利用者が関連する JS を手動でインポートする必要がない

JavaScript モジュールを作成する

wwwroot/js/fileDownload.js などの名前で JavaScript ファイルを作成し、export で関数を公開します。

wwwroot/js/fileDownload.js
export function triggerFileDownload(fileName, url) {
    const anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = fileName ?? '';
    anchorElement.click();
    anchorElement.remove();
}

IJSObjectReference でモジュールを読み込む

コンポーネント側では IJSRuntimeInvokeAsync<IJSObjectReference> を使って JS モジュールを動的にインポートし、そのモジュールの関数を呼び出します。IAsyncDisposable を実装してモジュールの参照を適切に破棄することも忘れずに行います。

@inject IJSRuntime JS
@implements IAsyncDisposable

<button @onclick="DownloadFile">ダウンロード</button>

@code {
    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./js/fileDownload.js");
        }
    }

    private async Task DownloadFile()
    {
        if (module is not null)
        {
            var fileName = attachment.FileName;
            var fileUrl = attachment.BlobUrl;
            await module.InvokeVoidAsync("triggerFileDownload", fileName, fileUrl);
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        try
        {
            if (module is not null)
            {
                await module.DisposeAsync();
            }
        }
        catch (JSDisconnectedException)
        {
            // Blazor Server の場合、SignalR 回線が切断された後に
            // DisposeAsync が呼ばれることがあるため例外を無視する
        }
    }
}

Blazor Server (サーバーサイド) の場合、コンポーネント破棄時に SignalR 回線が既に切断されていることがあります。その場合 JSDisconnectedException がスローされるため、try-catch で捕捉しています。

まとめ

Blazor で JavaScript 相互運用を使ってファイルダウンロードを実装する方法を紹介しました。JavaScript 関数を別途定義して引数で値を渡すのがポイントです。さらに JavaScript モジュール分離を使うことで、グローバル名前空間を汚染せずにクリーンな実装ができます。

詳細は公式ドキュメントを参照してください。

Microsoft (有志)

Discussion