[Tips]フロントエンドで複数ファイルを一括ダウンロード
この記事は何
フロントエンドの操作で複数ファイルを一括ダウンロードさせたかった時に、少しの躓きと気づきがあったので紹介します。
前提
- ブラウザはGoogle Chromeとします。他環境での動作は確認できていません。
- 想定するシーンは、ユーザーがブラウザ上でボタン操作を行うと、サーバーが裏でファイルを準備してS3に格納&ダウンロード用URLを生成してブラウザに返却し、ブラウザがそのURL先のファイルをダウンロードする、とします。ファイルは1つにまとめられておらず、複数ファイルを(ほぼ)同時にダウンロードします。
- サーバーサイドでファイルを1つにまとめておくのが親切かと思いますが、本記事ではそれはスコープ外とします。
1つのファイルをダウンロードする際の概要を図にすると以下のとおりです。
本記事では、一度のボタン操作で複数回同じファイルをダウンロードすることにして説明します。
結論
- JavaScriptで
<a>
を差し込んでクリックさせる方法は、連続するとブラウザ側でキャンセルされることがある。 - JavaScriptで
<iframe>
を差し込みsandbox=allow-downloads
としてあげれば連続ダウンロードが可能。
はじめに:フロントエンドでのダウンロード常套手段
フィーチャー技術ブログさんに大変わかりやすい記事があります。フロントエンドでJavaScriptを使って強制的にファイルをダウンロードさせるには、アンカー<a>
を作り、それをクリックさせる方法が使えます。
const anchor = document.createElement('a');
anchor.href = 'link/to/file';
anchor.download = 'file';
anchor.style.display = 'none';
document.body.appendChild(anchor);
anchor.click();
URL.revokeObjectURL(anchor.href);
document.body.removeChild(anchor);
この方法はシンプルで、単体ファイルダウンロードであればしっかりと動作します。
ところがこの方法で2つ以上のファイルをダウンロードさせようとすると最後のファイルしかダウンロードされない状況に遭遇します。(注:ダウンロードされる数は環境に依存する可能性があります。)
ためしに2つのファイルをダウンロードした際に、ブラウザの開発者ツールを開くと以下のようになっていました。
キャンセル済み…?Google Chromeの仕様でしょうか。連続したアンカークリック動作がキャンセルされているようです。確かな出典は探せていませんが、同じような状況はパラパラと確認できます。
上記の記事には同時に解決策も示されているのですが、本記事では、せっかくなので暫定的な対処方法も含めて順に示したいと思います。
暫定対策:ダウンロード間に待ち時間を設ける
各ダウンロード間に待ち時間を設ければ、先ほどのようなブラウザによるキャンセルは発生しませんでした。
for (let i = 0; i < downloadNum; i++) {
const anchor = document.createElement('a');
anchor.href = 'link/to/file';
anchor.download = 'file';
anchor.style.display = 'none';
document.body.appendChild(anchor);
// 1秒間隔をあけてダウンロードする
setTimeout(() => {
anchor.click();
URL.revokeObjectURL(anchor.href);
document.body.removeChild(anchor);
}, i*1000);
}
ただしこの方法はフレーキーでした。ファイルサイズが大きい場合には1秒間隔でもダウンロードがキャンセルされることがありました。
対策:iframeを使う
先ほどの記事にもあったように、iframeを使うことで安定して複数ダウンロードを行うことができました。
for (let i = 0; i < downloadNum; i++) {
const iframe = document.createElement('iframe');
iframe.src = "link/to/file";
iframe.style.display = 'none';
iframe.sandbox = 'allow-downloads';
document.body.appendChild(iframe);
}
ダウンロードが完了した時点でiframeを消してあげるのが一番良いと思いますが、上記サンプルにはそこまで入れ込んでありません。
なお、Safariはsandbox=allow-downloads
に対応していないため上記方法は使えません。
最後に
- 比較的シンプルにやりたいことが実現できました。
- アンカーを使う方法でダウンロードがキャンセルされるChromeの仕様をちゃんと出展を見つけたいと思います。
Discussion