uhyohyo.net

JavaScript初級者から中級者になろう

十二章第五回 File API

次に、Web近代史(?)中で重要な役割を果たすFile APIについて紹介します。

これはその名の通りJavaScriptからファイルを扱うためのものなのですが、ではどんなファイルを扱うのでしょう。

実は、ローカルのファイルです。つまり、ページを開いた利用者のパソコン(など)の中のファイルを見られるということです。

ただし、自由にファイルを見られてはセキュリティも何もないので、ユーザーが認めたファイルのみ開けるという安全仕様です。

ユーザーに認めてもらうには、ユーザーに選択してもらう必要があります。その方法というのはどうすればいいのでしょうか。実は、HTMLにこういうのがありましたね。

 <input type="file">
        

そう、このファイル選択のinput要素を使ってユーザーにファイルを選択してもらえばいいのです。

ファイルの取得

十二章第二回で紹介したとおりHTML5になってフォーム周りは大きく機能が強化されましたが、実はこのファイル選択においてもそうだったのです。

それでは、ここから選択済みファイルを取得するには、このinput要素のfilesプロパティを調べます。このfilesプロパティに入っているのが、FileListオブジェクトです。

名前の通りリストですから、もしかしたら複数ファイルが選択されている可能性があるかもしれないのですね。

そして、このFileListオブジェクトは、もはやNodeList(getElementsByTagNameなどで出現)などなどでお馴染みのlengthプロパティitemメソッドを持っています。ピンと来ると思いますが、lengthがファイルの総数で、itemメソッドは数値を指定するとn番目のファイルを返してくれるというわけです。

ちなみに、

filelist.item(0);	//0番目のファイルを取得

の代わりに、

filelist[0];

という省略記法が用意されているのも同じです。

さて、このようにして得たファイルの一つ一つを表すオブジェクトをFileオブジェクトといいます。

このFileオブジェクトというのがBlobオブジェクトの一種で(ElementがNodeの一種であるのと同様で、九章第三回で紹介した継承のような関係ですね)、ではBlobとは何かというとバイナリデータを表す、要するにファイルの中身そのものを表すオブジェクトであるということです。

そして、ではFileオブジェクトはBlobオブジェクトと何が違うかというと、Blobオブジェクトの機能(これはあとで紹介します)に加えてnameプロパティlastModifiedDateプロパティを持っています。これは読んで字のごとく、前者はファイル名で後者は最終更新日です。

ファイル名は当然文字列で、あとの最終更新日というのはDateオブジェクトで返ってきます。

それでは一旦、ここまでをサンプルで振り返ります。サンプル

ソースを見てみると、さっそくファイル選択のinput要素があって、

onchange="change(event)"

とあります。onchangeということは変更されたときということですから、今回の場合はファイルが選択されたときですね。

そして問題のchange関数では、ev.target.filesを取得しています。つまり、ev.targetが例のinput要素であるということです。このようなことは第三章以降何度も出てきていますのでいいですね。

そして今回は最初のファイルに決め打ちして、その名前を表示しているのです。

ちなみに、他にBlobのプロパティとして、size(そのファイルのバイト数)があります。

ファイルの中身を取得する

ところで、Blob(ファイルの中身データ)を取得したといっても、それをどうやって読み込んで利用するのでしょう。じつは、Blob自体はそういったメソッドを持っていないのです。

そこで登場するのがFileReaderオブジェクトです。このオブジェクトがBlobの読み込みを担当します。

この特徴は非同期読み込みであるということです。JavaScriptで非同期といった場合、意味するところはコールバックで結果を得るということです。

例えば、読み込むためにはいくつは方法がありますが、まずはreadAsTextメソッドを紹介します。つまりこれは、「読み込んでテキストを得る」ということです。テキストファイルを読み込むときなんかに使えますね。

ちなみに非同期の反対として同期というのもあります。同期ということは、その関数が全部処理をおこなってしまって、プログラム側は関数の処理が終わるまで待つということです。このようにプログラムが待たされることをブロッキングといいます。ブロッキングの典型的な例はalert(ユーザーがOKなどを押すまでプログラムは次に進まない)です。

ところが、今の時代同期というのはなかなか問題があるということで、最近は非同期が人気です。非同期というのは、さっきも出てきたコールバックによって結果を出るのですが、要するにこれは「終わったら呼んでね」ということです。今回の場合、readAsTextを「終わったら呼んでね!」と言って呼び出して、自分はその後も悠々と他の処理を続けているわけです。それで、結果が出たら呼ばれます。

呼ばれるというのは、要するに関数です。「終わったらこの関数を呼んでね」ということです。

さて、そんなことを考えつつ、いよいよFileReaderを見て行きましょう。

まず、FileReaderのインスタンスを作ります。

var reader=new FileReader();

次に、コールバック関数を設定してあげます。コールバックしてくれるタイミングというのもいくつかあるのですが(後述)、その中でも今回は読み込み完了のタイミングで関数を呼び出してもらうようにします。これは、onloadというプロパティに関数を代入します。

var reader=new FileReader();
reader.onload=function(e){
console.log("読み込みが終わりました");
};
        

そして、これで準備ができたのでいよいよreadAsTextを呼び出します。readAsTextは、第一引数にBlob(今回はFileなのでさっき読み込んだFile)を渡します。

var reader=new FileReader();
  reader.onload=function(e){
    console.log("読み込みが終わりました");
  };
  reader.readAsText(file);
        

ここで、readAsTextを呼び出す前にonloadに代入したのは、非同期なので、readAsTextを呼び出したあとonloadに代入するまでの間に完了してしまったら結果が得られなくて困るからです。

また、コールバック関数の第一引数にeと書いてますね。これは何かというと、実はおなじみのイベントオブジェクトです。FileReaderの場合、コールバックはイベントのように扱われるのです。

ですから、e.targetにFileReaderオブジェクト、つまり今回の場合はreaderが入っています。複数のFileReaderで同じコールバック関数を使いまわしたいときなどに利用できるでしょう。

さて、読み込みが終わった時点で、読み込み結果はどこに入っているのかというと、FileReaderのresultプロパティに入っています。つまり、今回の場合reader.resultです(もちろん、e.target.resultでも同じです)。

ということで、ここまでのを試すサンプル2を見ましょう。

ファイルを選択するとコンソールに中身が表示されるのを確認できたと思います。

ところで、テキストというとエンコードの問題がつきまといます。そのファイルをどんなエンコードで読み込むかは第二引数で指定します。第二引数がない場合はUTF-8になります。他に"UTF-16"とか、"Shift_JIS"などなどが使えます。

バイナリファイルの読み込み

さて、では読み込みたいファイルがテキストでない場合はどうしたらいいかというと、readAsTextではなく別のメソッドを使います。

そこで次に紹介するのが、readAsArrayBufferです。これは、そのBlobをArrayBufferとして読み込むということです。もちろん読み込んだ結果はreadAsTextと同じように、resultプロパティに入っています。

このArrayBufferというのは要するに「バイナリデータが入っているオブジェクト」ということなのですが、それだけだとBlobとあまり変わりません。ArrayBufferの特徴は、実際にメモリ上に連続する領域が確保されているということです。ファイルをBlobとして得られた段階ではまだそのファイルをメモリ上に読み込んでおらず、それを実際にメモリ上に(文字列やArrayBufferの形で)読み込むのがFileReaderなのだということですね。

しかし、Blobに対するFileReaderのように、さらに別のオブジェクトを使わないと内容を読むことができないという点はBlobと同じです。なかなか面倒ですね。

まあでも、このArrayBufferというのは先々また出てくる、わりと汎用的なものです。そして、ArrayBufferを読み込むために使うのが型つき配列です。

つまり、ArrayBufferというのがバイナリデータですから、それらを一定間隔で区切っていって配列にして読み込もうというというわけです。

配列とはいっても、今まで扱ってきたJavaScriptの配列とはかなり違うので、別物のオブジェクトと考えたほうがよいでしょう。考え方としては、C言語とかの配列に近いです。

型つき配列にはいくつか種類があるのですが、まずはよく使いそうなUint8Arrayから紹介しましょう。Uint8とは、「8ビットで符号なし整数」ということです。Cでいう「unsigned char」にあたるものですね。

8ビットということはすなわち1バイトですから、バッファを1バイトずつに区切って配列のようにしたものだということです。これは都合がいいですね。

他に、UがないInt8Arrayは符号あり、すなわち8ビットなら-128〜127の範囲の整数になります。ほかに、Uあり・なしそれぞれに16と32があります。さらに、Float32ArrayとFloat64Arrayというのがあります。これらの型つき配列を総称してArrayBufferViewといいます。

さて、それでは都合のよいUint8Arrayを例にして使い方を見ていきます。他のも使い方は同じです。

var arr=new Uint8Array(buffer);

このように新しいUint8Arrayオブジェクトを作ります。ここで、bufferという変数はArrayBufferとします。

Uint8ArrayをはじめとするArrayBufferViewについては機能がほかにもいろいろあるのですが、ここではあまりくわしく解説しません。このUint8Arrayを通してArrayBufferを操作していくのです。ここで注意すべきことは、一つのArrayBufferに対して複数のArrayBufferViewを用意した場合、一方を操作するともう一方にも反映されるということです。これは、ArrayBufferViewはArrayBufferが確保するメモリを操作するインターフェースであり、結局操作するのは同じArrayBufferだからです。

それではこのArrayBufferViewの機能を見ていきますが、まずはlengthプロパティで配列の長さを取得できます。Uint8Arrayならば、1要素1バイトなのでArrayBufferのバイト数(実はこれはArrayBufferのプロパティbyteLengthで取得できます)と一致しますが、16とか32になるとまた異なってくるでしょう。

そして、この場合はarr[0]で0番目の要素を取得できるなど、FileListなどと同じように扱います。

かなり端折った説明でしたが、ここまで分かれば読み込んだファイルのバイト列を取得することができるはずです。

ということでサンプル3を見ましょう。

結構長々と解説しましたが、ソースコードになるとやっていることは結構単純です。とにかくこれで、バイナリファイルの読み込みも可能になりました。

型付き配列については紹介していないことがまだあります。くわしくは調べてみましょう。

readAsDataURL

実は、テキストとバイナリの2つだけかとおもいきや、まだ読み込み用メソッドがあります。それがreadAsDataURLです。

このDataURLというのは、http://などから始まるのではなく、data:から始まる特殊なURLのことです。これは何かというと、URLというのはそのアドレスになるリソースを示すものです。そこで、dataURLというのはその読み込んだ先にあるべきリソースのバイナリデータを直接URLに書いてしまったものです。

例えば、次のようなDataURLがあるとします。

data:text/html,%3c%21doctype%20html%3e%3chtml%3e%3cbody%3e%3ch1%3etest%3c/h1%3e%3c/body%3e%3c/html%3e

これをブラウザのアドレスバーに入力してみましょう。「test」と出ますね。ソースを表示すると次のように出るはずです。

<!doctype html><html><body><h1>test</h1></body></html>

つまり、「data:text/html,%3c...(略)」というURLを開いたということは、 この「<!doctype...(略)」という内容のページを読み込んだのと同じ事だということです。

このように、ブラウザが読み込んだとき、あたかもどこかからその内容を取ってきたかのように振る舞うのがdataURLです。

さて、readAsDataURLでは、ファイルの内容をこのdataURLに変換させて読み込むことができるというのです。

これの意味するところはつまり、そのURLをブラウザに読ませればブラウザ上に表示できるということです。

ということで、これもサンプルを提示します。

画像を読み込ませてやるとその内容が表示されたと思います。できたimg要素のsrc属性を調べると、長いDataURLになっていることがわかります。

このように、dataURLというのはユーザーが選択したものをブラウザに読み込ませたいときに使える場合があります。

URL.createObjectURL

しかし、実際にはユーザーが選択したファイルを表示したい場合にreadAsDataURLを使うことは少ないでしょう。URLが長大になってしまい扱いにくいからです。そこで、もっと便利な方法が用意されています。それがURL.createObjectURLです。これはURLというオブジェクトが存在して、それが持つcreateObjectURLメソッドであることは今更言うまでもありませんね。

このメソッドにBlobを引数として渡すと、オブジェクトURLと言われる特殊なURLが生成されます。これは当然その場でだけ有効なURLで、他の人に渡してもデータが渡せるわけではありません。readAsDataURLで得られたdataURLの代わりに、このcreateObjectURLで得られたオブジェクトURLをimg要素などに渡すことができます。

すると、前述の通りBlobはまだ実際にはメモリ上にファイルが読み込まれていませんから、ブラウザがオブジェクトURLを処理する段階で内部処理として実際にファイルを読み込んでくれます。

これは、dataURLに比べて利点があります。ひとつは、dataURLはファイルの内容を全て文字列として表す必要があるため、ファイル内容が大きくなるほど長い文字列になり、メモリも食います。

それに対してcreateObjectURLでは、その段階ではまだファイルを読み込まず、img要素などで使われる段階で読み込まれます。

これが特に役に立つのは、audio要素やvideo要素で音楽・動画を再生する場合です。これらはファイルサイズが非常に大きい場合があるので、普通一度に全てメモリ上にファイルを読み込むわけではなく、必要な部分を順次読み込んでいくようになっています。createObjectURLで生成されたURLではこの動作が行われますが、dataURLの場合はそれを生成する時点で全て読み込んでいる必要があるため時間がかかったりメモリを多く消費したりします。

ですから、readAsDataURLを使う機会は少ないかもしれません。

ちなみに、createObjectURLで得られたURLは、使い終わったらURL.revokeObjectURLメソッドを呼び出して(第一引数にオブジェクトURL)やるとよいでしょう。これは、そのURLはもう使い終わったとブラウザに宣言することで、ブラウザの記録から消去してやるメソッドです。わざわざそんなことをする意味ですが、ブラウザによってはcreateObjectURLで作れるURLの数に限度があり、revokeObjectURLを使用してURLを開放してやらないと新しいのを作れなくなることがあるようです。

この開放という操作はBlob URLを扱う上で重要なようで、最近ではURL.createForというメソッドも登場しました。これはcreateObjectURLと同じ動作ですが、こちらで作ったURLは使い終わったらrevokeObjectURLしなくても自動で開放されます。ただし、createForはまだブラウザのサポートが進んでいないようです(2014年7月現在)。

FileReaderのその他の機能

コールバックとして、ファイルの読み込みが完了した場合を紹介しましたが、実はほかにもコールバックしてくれる場面があるので、対応するプロパティ名とともに紹介します。ちなみに、名前から分かる通りこれらは実はイベントです。

onloadstart
ファイルの読み込みを開始したとき。
onprogress
ファイルの読み込みが進行したとき。(何度も発生する可能性があります)
onabort
ファイルの読み込みが中断したとき(後述)。
onerror
ファイルの読み込みに失敗したとき。
onload
ファイルの読み込みが正常に最後まで完了したとき。
onloadend
ファイルの読み込みが終了したとき(失敗した場合も含む)。

よく使いそうなのはonload,onerrorですね。あと、onprogressなんかもたまに使い道があるかもしれません。

onabortですが、中断するのはどういう場合かというと、FileReaderのabortメソッド(引数なし)を呼ぶと中断させることができます。