インターネットブラウザでWebアプリを作るととても便利なんですよ。
デスクトップアプリケーションと同じ様な機能を持たせると、それはもうアプリのインストールなんか必要なくて、ブックマークしておくか、デスクトップにエイリアスなどのリンクを置いておくだけでいいですからね。
もちろん、できない事のデメリットもあるけど、デスクトップアプリにはできないこともできてしまうメリットもあります。
そんなWebアプリを作っている時に、ファイルダウンロードする機能を持たせていたら、ダウンロードした直後にページがリロードしてしまうという意味のわからない事象が発生したので、その原因究明までの道のりを備忘録しておきます。
Webアプリのファイルダウンロード機能
検索するとたくさん出てくるのでもはや定番機能と言ってもいいと思いますが、ホームページのダウンロードをJavascriptで便利に行う機能です。
とりあえず、どんな機能なのかというのをシンプルにしたコードを書いておきます。
対象のコード
const save_data = {
a : 1,
b : 2,
c : 3,
}
// データを文字列に変換
const json = JSON.stringify(save_data)
// エンコードする
const uint8Array = new TextEncoder().encode(json)
// バッファを取り出す
const buffer = uint8Array.buffer
// blobを作る
const blob = new Blob([buffer], {type: "application/octet-stream"})
// ダウンロードするURLに変換する
const url = window.URL.createObjectURL(blob)
// Aタグ生成
const a = document.createElement("a")
document.body.appendChild(a)
// URLをセット
a.href = url
// ダウンロードするファイル名をセット
a.download = "sample_"+ (+new Date()) +".dat";
// 見えないようにする
a.style.display = "none"
// 自動でクリックして、ダウンロードダイアログを表示する
a.click()
// 生成したurlを削除してメモリ解放する
window.URL.revokeObjectURL(url)
上記のコードを、Webアプリのデータダウンロードをダウンロードするボタンを押した時に実行すると、save_data部分のデータをファイルに格納して保存します。
サンプルコードでは、save_dataは簡単な連想配列データになっていますが、実際はこれがバイナリーデータになるため、mimeタイプを"application/octet-stream"にセットしています。
単なるテキストであれば、"text/plain"、JSONファイルであれば、"text/json"としておけば言いのですが、今回は特定できないバイナリデータという事で、"application/octet-stream"をセットするようにしました。
MIMEが原因?
ネットで検索してみると、多くの場合のダウンロード形式は、テキストやJSONが多いので、もしかしたら、"application/octet-stream"というMIMEが、リロードの原因なのではないかと考えました。
const blob = new Blob([buffer], {type: "application/octet-stream"})
この箇所を、以下に変更してみました。
const blob = new Blob([buffer], {type: "text/plain"})
でも、結果変わらず・・・
データが大きいのか?
上記のサンプルコードでは、簡単な連想配列になっているのですが、実際は1MBぐらいのバイナリデータを扱っているので、そのデータのサイズがブラウザの許容よりも超えているのが原因ではないか?と考えてみました。
上記の連想配列でテキストMIMEで行っても、やっぱり状況は変わらず・・・
Aタグの仕様なのか?
もしかしたらAタグの仕様がブラウザのバージョンアップで変わってしまったのか?と考えてみました。
もしそうだとしたら、これは少し厄介な案件になってしまうな〜と思って、以前に同じ様なコードで開発したツールでFile-Saveの機能を作っていたので試してみました。
以前作ったツール:
https://image-motion.mynt.work/
これは問題なくダウンロードできました。(リロードしませんでした)
ChatGPTに聞いてみた
ここまできたら、もはや自分の知見ではお手上げ状態なので、ChatGPT先生にお聞きするしかありません。
上記コードで、リロードしないようにする方法を教えて欲しいとお願いした所、次のような返答をもらえました。
a.click()の発動タイミングを変えてみる
a.click()以下を以下のように変えるという内容。
// イベントハンドラでクリックイベントを手動で発火し、その後にデフォルトの動作を防ぐ
a.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
a.click();
window.URL.revokeObjectURL(url);
});
// 手動でクリックを発火
const evt = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
a.dispatchEvent(evt);
コメントにも書いてますが、aクリックでリロードするのであれば、クリック挙動のみを残して、preventDefault()という動作を停止する命令を加える方法です。
結論を言うと、この方法でもダメでした。
でも、MouseEventという少し特殊なクリックイベント発火を覚えられたので、普段あまり使わないですが、これば別の機会で使えそうだというおまけメリットを感じた提案でした。
発火タイミングを変更する
次に提案してもらったのは、setTimeoutを使って、a.click()の発火タイミングを変えるというモノです。
// setTimeoutを使用して非同期的にクリックをトリガー
setTimeout(() => {
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}, 0);
結果、これもダメ。
ローカルのhttpプロトコルで実行しているので、httpsで試す
確かにこれは一理あります。
最近のWebブラウザは、https以外のプロトコルでは、セキュリティエラーがでるものがたくさん増えてきたので、試してみる価値はあるので、VScodeのLiveServerで実行していたのを、Dockerに切り替えて実行
成功!
原因判明!!!
VScodeのLiveServerプラグインが原因だと言うことがここで明確にわかりました。
このプラグインは、手軽にローカルのWebサーバーで、簡単なHTML,CSS,Javascriptの表示と挙動の確認ができるというのであまりのもお手軽なツールなので最近多様していましたが、
このツール以前もブログで書きましたが、勝手にリロードするというありがた迷惑仕様が原因でした。
以前書いたブログたち
[不具合注意報] VScodeのLiveServerを使うと想定外のscriptタグが埋め込まれる件
[不具合注意報] VScodeのLiveServerでHTMLが正常に読み込まれない不具合を発見!
このツール、なんで勝手にリロードするのかというと、プログラムの更新があった時に、すぐに表示反映するというまさにLiveServerの機能なわけでした。
あとがき
実は以前もこのコードは何度も書いて実装してきた経験があるので、これまでとの違いを考えるといいかと始めはナメて掛かっていました。
でも、このトラブル回避まで1時間以上かかる結果になったので、改めて基本に立ち返る思考の大切さを思い出しましたね。
とりあえず、このブログを書いて、こうした不具合は成仏させてあげるに限ります。
というわけで、今回のトラブル騒ぎは一件落着したけど、LiveServerは改めて、使う場面を選ぶという事を心の底から理解できました。
やっぱDockerなのかな・・・CGIも動くし・・・
0 件のコメント:
コメントを投稿