2016 07 04

HTML5 File API : write

昨日の続き。

手元にある JavaScript リファレンス 第6版 には、ファイル出力に関しては何の記載も無いが、ググったら出てきた。 そして、ググったら出てくるものがリファレンスには無いことの意味を思い知らされるのだった。

write

テキストエリアの入力値を指定のファイル名で保存するサンプル。

weite_sample.html

<!DOCTYPE html> <html> <head> <title>File-API Write Sample</title> <meta charset="UTF-8"> <script> function doDownload() { var textdata = document.getElementById( "dspArea" ).value; var filename = document.getElementById( "filename" ).value; var blob = new Blob( [ textdata ], { "type" : "text/plain" } ); if ( window.navigator.msSaveBlob ) { // IE window.navigator.msSaveBlob( blob, filename ); } else { var a = document.createElement( "a" ); a.download = filename; a.href = window.URL.createObjectURL( blob ); var div = document.getElementById( "controls" ); div.appendChild( a ); a.click(); div.removeChild( a ); } } </script> </head> <body> <div id="controls"> <input type="text" id="filename" value="output.txt" /> <input type="button" value="download" onclick="doDownload()" /> </div> <div id="contents"> <textarea id="dspArea" rows="10" cols="30"></textarea> </div> </body> </html>

結果としてローカルファイルが作られるのだが、形式的にはダウンロードして保存。 ということで、幾つか制約がある。

同じ blob なのだが、入力時には指定できたエンコーディングが、出力時には指定できない。 まあ、個人的には UTF-8 さえできれば他ができなくても特に困らないので問題ないんだけどさ。 保存先がダウンロードフォルダ限定ってのも、特に問題は無いかな。 セキュリティを考えると、これが正解だと思うし。

ダウンロードフォルダに同じファイル名が存在すると、勝手にファイル名に (1) なんて付けて名前の重複を防いでくれるのは、むしろありがたい。

面倒なのはやはりブラウザによる実装の違い。 読込よりもはるかに違いが大きい。

IE 11, Edge

実は最も簡単に実装できるのがIE用。

  1. Blobオブジェクトを生成する。
  2. window.navigator.msSaveBlob() を実行する。

これだけ。

Blobオブジェクトを作る時、第1引数が配列であることは注意が必要。 これはブラウザに関係無く同じ。 この配列の先頭要素に、出力したいデータを設定する。 第2引数はMIMEタイプだが、無くてもかまわない。

Chrome, FireFox

ダウンロード用のアンカーをクリックするという体裁にする必要がある。 a要素にはdownload属性が拡張されていて、これがダウンロードするファイル名になる。 クリックした時のイベントハンドラで、遷移先となるhref属性を設定することでダウンロードとなる。

ポイントだけだとこんな感じ。

<script> function doDownload() { var textdata = document.getElementById( "dspArea" ).value; var blob = new Blob( [ textdata ], { "type" : "text/plain" } ); document.getElementById( "download" ).href = window.URL.createObjectURL( blob ); } </script> 〜中略〜 <a href="#" id="download" download="output.txt" onclick="doDownload()">download</a>

これはa要素を最初から書いているが、上の方のサンプルでは、ボタンクリックの処理の中で一時的に作ってクリックし、終わったら消している。 ファイル名が固定なのは何かと不便だし、かといって毎回差し替えるなら手間は変わらないし、何よりIEと共用するのにアンカーとボタンが並存するのはかっこ悪いし。

どうせ消すならと、a要素を本体のDOMツリーに追加しないままでいけないか試してみたのだが。

// div.appendChild( a ); a.click(); // div.removeChild( a );

つまり、こんな状態でやってみたのだが、クリックイベントが発火しなかった。 まあ、DOMツリー上の位置がはっきりしないとイベントの伝播とか処理できないし、これはしょうがないか。

Safari

すっかり忘れていたのだが、Windows に対してはもうずいぶん前に Safari の提供は終わってるんだね。 Windows版のバージョンは5だったのだが、これだと古くて何もできない。

で、Mac版だが、これはこれで駄目。 ダウンロードされるのでは無く、指定したMIMEタイプに従ってブラウザ上で表示される。 要は画面遷移しているのだ。

ちなみに、このときブラウザのアドレス欄には blob:null/16c0de0e-… なんて表示されている。 後ろのセッションIDみたいなのは、毎回変わる。

さらにここから名前を付けて保存ができるのだが、ファイル名は Unknown になっていて、download属性の指定はきっちり無視されている。

その他、愚痴など

Microsoftが提示しているサンプルコードと、ググって出てきたコードを合体して、最初はこんな風に書いた。

<!DOCTYPE html> <html> <head> <title>File-API Write Sample</title> <meta charset="UTF-8"> <script> function doDownload() { var textdata = document.getElementById( "dspArea" ).value; var blob = new Blob( [ textdata ], { "type" : "text/plain" } ); if ( window.navigator.msSaveBlob ) { // IE window.navigator.msSaveBlob( blob, document.getElementById( "download" ).download ); } else { document.getElementById( "download" ).href = window.URL.createObjectURL( blob ); } } </script> </head> <body> <div id="controls"> <a href="#" id="download" download="output.txt" onclick="doDownload()">download</a> </div> <div id="contents"> <textarea id="dspArea" rows="10" cols="30"></textarea> </div> </body> </html>

最初からダウンロード用のa要素を書いておき、こいつのクリックイベントの中で、IEとその他を分岐する形。 これがIEだとうまくいかない。

IE 11 だと、ファイル名が無視される。 保存のダイアログが表示されるのだが、そこには Mac 版の Safari の時のようなセッションIDっぽいものが表示されている。 どうも download 属性が取れていないらしい。

ドットで繋げる形の document.getElementById( "download" ).download ではなく document.getElementById( "download" ).getAttribute( "download" ) とすると上手くいった。 ああ、そういう…なんて軽く溜息一つ。

Edge は、ファイル名の取得は問題ないのだが、別の問題が。

クリックを実行すると、ダウンロードフォルダにまず write_sample.html つまり自分自身がダウンロードされ、その後に output.txt がダウンロードされる。 どうやらダウンロード用のアンカーのクリックによる画面遷移(=ダウンロード)と、そのイベント処理の中でやっているファイル出力が別扱いで実行されているらしい。

本来やりたい方をコード中で明示的に書いているのだし、だったらその後でイベント処理を止めてやればいいのではないか。 そう考えて、ファイル出力の後にデフォルト動作のキャンセルを入れてみたのだが。

function doDownload( e ) { var textdata = document.getElementById( "dspArea" ).value; var blob = new Blob( [ textdata ], { "type" : "text/plain" } ); if ( window.navigator.msSaveBlob ) { // IE window.navigator.msSaveBlob( blob, document.getElementById( "download" ).download ); e.preventDefault(); } else { document.getElementById( "download" ).href = window.URL.createObjectURL( blob ); } }

write_sample.html がダウンロードされるのはそのまま。 output.txt がダウンロードされなくなった。

何故? と、こちらは深い溜息。

そんなこんなの紆余曲折を経て、最終的に最初のサンプルコードに辿り着いたのだった。