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 で関数を公開します。
export function triggerFileDownload(fileName, url) {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
}
IJSObjectReference でモジュールを読み込む
コンポーネント側では IJSRuntime の InvokeAsync<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 モジュール分離を使うことで、グローバル名前空間を汚染せずにクリーンな実装ができます。
詳細は公式ドキュメントを参照してください。
Discussion