仕様解説
File API入門
第1回 File APIとFileReader APIの利用

第1回目はFile APIでできることを概観し、File APIとFileReader APIを利用し、ローカルにある画像ファイルを選択して、ブラウザでプレビューを表示するアプリを作ります。

2013年06月27日発行

目次

File APIとは

File APIでできることを簡潔に言うと、ローカルマシンに存在するファイルをJavaScriptから操作することです。例えば、次のような実装が可能です。

  • ローカルにある画像をアップロードする前にプレビューを表示する
  • ローカルにある画像をブラウザで加工してダウンロードする
  • テキストファイルをブラウザで解析する
  • データをJSONファイルとしてエクスポートして、インポートする
  • 大きなファイルを分割してサーバーに送信する

当シリーズでは2回に分けて、1回目はFile APIとFileReader APIを利用し、ローカルにあるファイルを選択して、ブラウザでプレビューを表示する簡単なアプリを書くことをゴールとします。

次回、2回目ではBlob constructingとBlob URLsを利用して、なんらかのデータをダウンロードするようなアプリを書くことをゴールとします*。

*注:シリーズでは触れない仕様

ローカルのサンドボックス化されたディレクトリを操作できるFileSystem API、ファイルの書き込みが行えるFileWriter APIについては、本シリーズでは触れません。

本シリーズで扱うAPIの対応ブラウザとバージョン

まずは扱うAPIに対応しているブラウザとそのバージョンを見てみましょう*。

*注:サポートの詳細

この表はCan I use...を参照しています。また以下のような方針で作成されています。

  • Partialサポートは除く
  • Blob constructingは、deprecatedになったBlobBuilder APIを除く

AndroidブラウザではBlob constructingがサポートされていません。

API
File API13+3.6+6+11.1+10+6+3+
FileReader API13+3.6+6+11.1+10+6+3+
Blob constructing20+13+6+12.1+10+6+--
Blob URLs8+4+6+15+10+6+4+

それでは、今回扱うFile APIとFileReader APIの概要とサンプルを見ていきましょう。

File APIの概要:FileListとFile

単一のファイルを扱うFile、Fileをオブジェクトとして持つFileListがあります。

input[type="file"]に渡したファイル(FileList)をfilesプロパティで参照できます。またinput[type="file"]要素にはmultiple属性を付けることで、複数のファイルを選択できるようにできます。

FileListは配列ではなくオブジェクトですので、注意してください。

File APIサンプル

ソースコードは次のようになっています。

<input type="file" id="file">
var inputFile = document.getElementById('file');
 
function fileChange(ev) {
  var target = ev.target;
  var files = target.files;
 
  console.log(files);
}
 
inputFile.addEventListener('change', fileChange, false);

なんでもいいのでJPEG画像ファイルを選択してみましょう。コンソールにはオブジェクトが表示されます。

FileList { 0: File, length: 1, item: function }
 
// FileListを展開
{
  0: File
  length: 1
}
 
// Fileを展開
{
  0: {
    lastModifiedDate: Sat Jun 22 2013 23:45 GMT+0900 (JST),
    name: "image.jpeg",
    size: 33792,
    type: "image/jpeg"
  },
  length: 1
}

このように、FileListオブジェクトはFileオブジェクト群を内包しています。

FileListオブジェクトには、lengthプロパティがあり、Fileオブジェクトにはインデックスのようなキーが割り振られていますが、FileListオブジェクトは配列ではありません。

Fileオブジェクトのプロパティ

Fileオブジェクトは選択したファイルの情報を内包しています。

プロパティ解説
lastModifiedDateファイルの最終更新日(Dateオブジェクト)
nameファイル名
sizeファイルのサイズ(byte)
typeファイルのMIMEタイプ

Fileオブジェクトにはtypeプロパティや、sizeプロパティがありますので、MIMEタイプやファイル容量をフロント側で制限することも可能です。

10KB以下のJPEG画像ファイルしか選択できないサンプルを用意しました。

var inputFile = document.getElementById('file');
function fileChange(ev) {
  var target = ev.target;
  var file = target.files[0];
  var type = file.type; // MIMEタイプ
  var size = file.size; // ファイル容量(byte)
  var limit = 10000; // byte, 10KB
 
  // MIMEタイプの判定
  if ( type !== 'image/jpeg' ) {
    alert('選択できるファイルは10KB以下のJPEG画像だけです。');
    inputFile.value = '';
    return;
  }
 
  // サイズの判定
  if ( limit < size ) {
    alert('10KBを超えています。10KB以下のファイルを選択してください。');
    inputFile.value = '';
  }
}
 
inputFile.addEventListener('change', fileChange, false);
File APIサンプル - MIMEタイプ、容量制限あり
10KBより大きな容量のJPEG画像以外を選択するとアラートが表示される。

FileReader API

FileReaderでは、Fileオブジェクトのファイルを実際に読み込みます。プレビューとして表示するというような動作はFileReaderを利用して行います。

まずはコンストラクタからインスタンスを作ります。

var reader = new FileReader();

FileReaderの読み込みメソッド

そしてFileReaderには非同期な3つの読み込みメソッドが定義されていますので、いずれかを使ってファイルを読み込みます。

メソッド引数解説
readAsArrayBufferBlob or FileファイルをArrayBufferとして読み込む
readAsTextBlob or Fileファイルをテキストとして読み込む
readAsDataURLBlob or FileファイルをDataURLとして読み込む

また、W3Cの仕様書には記載されていませんが、次の読み込みメソッドも利用できます。

メソッド引数解説
readAsBinaryStringBlob or Fileファイルをバイナリ形式で読み込む

次のようなコードになります。

reader.readAsDataURL(file);

FileReaderのその他のメソッドとプロパティ

読み込みメソッド以外には、次のようなメソッドがあります。

メソッド解説
abort読み込みを破棄する

また、次のようなプロパティがあります。

プロパティ解説
state読み込みステータス
result読み込み結果

resultreadAs~を実行した後、次に解説するonloadイベントのタイミングで取得できるようになります。

FileReaderのイベント

readAs~でファイルの読み込みを実行または、読み込みをabortすると、FileReaderのイベントが発行されます。

イベント解説
onloadstart読み込みを開始した
onprogress読み込み中
onabort読み込みを破棄した
onerrorエラーが発生した
onload読み込みが成功して完了した
onloadend読み込みが完了した(エラーを含む)

読み込み完了時にデータを表示する

とても簡単に書けます。HTMLは最初のものと同じです。

var inputFile = document.getElementById('file');
var reader = new FileReader();
 
function fileChange(ev) {
  var target = ev.target;
  var file = target.files[0];
  var type = file.type;
  var size = file.size;
 
  if ( type !== 'image/jpeg' ) {
    alert('選択できるファイルはJPEG画像だけです。');
    inputFile.value = '';
    return;
  }
 
  reader.readAsDataURL(file);
}
 
function fileLoad() {
  console.log(reader.result);
}
 
inputFile.addEventListener('change', fileChange, false);
reader.addEventListener('load', fileLoad, false);

コンソールを見ると、次のような出力が確認できます。

=> console(画像を選択すると表示されます。長すぎるので省略しています)
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEA...
File Readerサンプル

お気づきかと思いますが、readAsDataURLで読み込んだ画像は、img要素のsrcに入れて、そのまま表示することができます。

プレビューを表示するアプリを書いてみる

それでは、準備もできたので、選択したファイル(複数可)の名前とファイルサイズ、プレビューを表示するような、簡単なアプリを書いてみましょう。

PreviewImg(完成サンプル)
ファイルは複数選択できる仕様。ファイル選択ダイアログが開いたら、command(ctrl)+クリックで読み込みたい画像を選択できる。複数ファイルを指定すると、指定したファイル数が表示される。

なお、アプリではjQueryとUnderscore.js、Backbone.jsを利用します。Underscore.jsとBackbone.jsがよくわからない場合は、過去のシリーズと連載をご覧ください。

PreviewImg:HTMLはシンプル

まずHTMLですが、UIはすべてJavaScriptから吐き出すので、ベースになるHTMLはとてもシンプルです。

<div class="mod-previewImg"></div>

PreviewImg:Modelを書く

Modelもとてもシンプルに書けます。

file attributeFileオブジェクトを持たせます。Viewから参照できるように、dataURLにはFileReaderでの読み込みが完了した際に、URLを保存します。

また、このModelはFileReaderも個別に扱います。readFileメソッドを持たせて、ここではFileオブジェクトをdataURLとして読み込みます。FileReaderloadイベント完了を監視し、読み込みが完了したらイベントを発行するような仕組みです。

// Fileオブジェクトを内包するモデル
var FileModel = Backbone.Model.extend({
 
  defaults: {
    file: '',
    dataURL: ''
  },
 
  initialize: function(attr) {
    var self = this;
 
    // FileReaderを準備しておく
    self.reader = new FileReader();
    self._eventify();
  },
 
  _eventify: function() {
    var self = this;
 
    // ファイルの読み込みが完了したらdataURLに値をセット
    // その後、readerLoadイベントを発行する
    self.reader.addEventListener('load', function() {
      self.set('dataURL', self.reader.result);
      self.trigger('readerLoad', self);
    }, false);
  },
 
  // ファイルを読み込むメソッド
  readFile: function() {
    var self = this;
    var reader = self.reader;
 
    reader.readAsDataURL(this.get('file'));
  }
 
});

PreviewImg:Collectionを書く

Collectionはさらにシンプルです。

各モデルのreadFileメソッドを呼ぶためのreadFilesメソッドを持っているだけです。つまり、ViewからはCollectionのreadFilesメソッドを呼ぶだけでいいというわけです。

Modelが発行したreaderLoadイベントはCollectionでもキャッチできるので、ViewからはこのCollectionのイベントを監視するようにすればいいだけです。

// FileListを元にしたコレクション
var FileCollection = Backbone.Collection.extend({
 
  model: FileModel,
 
  // それぞれのModelのreadFileコールする
  readFiles: function() {
    var self = this;
 
    this.each(function(file) {
      file.readFile();
    });
  }
 
});

PreviewImg:Viewを書く

Viewは、ModelとCollectionに比べると、少し長くなりますが、分けて考えれば、わかりやすいです。大きく分けて、行っていることは以下の2つです。

初期化

Viewの役割としては、初期化時に、以下のことを行います。

  • Collectionを初期化する
  • input[type="file"]や決定ボタン、プレビューエリアの枠をレンダリングする
  • Collectionが発行するイベントを監視しておく

ファイルの選択と、プレビューボタンのクリック

イベントデリゲーションで、各要素に動作を決めます。

  • input[type="file"]の選択をしたら、FilesからCollectionをresetする
  • 決定ボタンがクリックされたら、Collectionからプレビューエリアをレンダリングする

実際のコードを見てみましょう。

// UI部分
var FileView = Backbone.View.extend({
 
  // イベントデリゲーション
  events: {
    'change .file'  : '_onFileChange',
    'click .submit' : '_onClickButton'
  },
 
  initialize: function($input, $preview) {
    var self = this;
 
    // Collectionを初期化しておく
    self.collection = new FileCollection();
    // UIをレンダリングする
    self.render();
    self._eventify();
  },
 
  _eventify: function() {
    var self = this;
 
    // Modelが発行するイベントをキャッチして
    // プレビューエリア内をレンダリングする
    self.collection.on('readerLoad', self.renderPreview, self);
  },
 
  // UIをレンダリングするメソッド
  render: function() {
    var self = this;
    var $el = self.$el;
 
    $el.append(self.html);
 
    self.$input = $el.find('.file');
    self.$preview = $el.find('.preview');
  },
 
  // プレビューエリア内をレンダリングするメソッド
  renderPreview: function(model) {
    var self = this;
 
    self.$preview.append(
      _.template(
        self.previewHtml,
        { model: model }
      )
    );
  },
 
  // UI部分のテンプレート
  html: [
    '<p>選択できるファイルはJPEG画像ファイルです。<br>',
    '<input type="file" class="file" multiple></p>',
    '<p><input type="button" class="submit" value="選択したファイルのプレビューを表示する"></p>',
    '<div class="preview"></div>'
  ].join(''),
 
  // プレビューエリアに追加するテンプレート
  previewHtml: [
    '<p>',
      '<img src="<%= model.get(\'dataURL\') %>"><br>',
      '<span class="name">name: <%= model.get(\'file\')[\'name\'] %></span><br>',
      '<span class="size">size: <%= model.get(\'file\')[\'size\'] %>(byte)</span>',
    '</p>'
  ].join(''),
 
  // Filesオブジェクトを扱いやすいように作り直す
  // ModelにはFileオブジェクトを直接渡せないので、
  // ひとつ下げて持たせておく
  _getFiles: function(files) {
    var self = this;
    var ret = [];
 
    _.each(files, function(file, key) {
      if ( !files.hasOwnProperty(key) || key === 'length' ) {
        return;
      }
      // fileのMIMEタイプはimage/jpegだけを許可する
      // それ以外は無視する
      if ( file.type !== 'image/jpeg' ) {
        return;
      }
      // ひとつ下げる
      ret.push({ file: file });
    });
    return ret;
  },
 
  // 配列filesからCollectionを作る
  _getCollection: function(files) {
    var self = this;
 
    self.collection.reset(files);
  },
 
  // input[type="file"]でファイルが選択されたとき
  _onFileChange: function(ev) {
    var self = this;
    var target = ev.target;
    var files = self._getFiles(target.files);
 
    self._getCollection(files);
  },
 
  // プレビューエリアを表示するボタンをクリックしたとき
  _onClickButton: function() {
    var self = this;
 
    // プレビューエリアを空にする
    self.$preview.empty();
 
    // Collectionに何もない場合はアラートを出す
    if ( !self.collection || !self.collection.length ) {
      return alert('先にファイルを選択してください');
    }
    // 問題なければCollectionのreadFilesメソッドをコールする
    self.collection.readFiles();
  }
 
});

まとめ

サンプルを通して、File API、FileReader APIについて理解していただけたでしょうか。次回紹介する、Blob constructingとBlob URLsを利用すると送信するデータのスライスや、データのダウンロードまでが可能になり、さらにFile APIの幅が広がるでしょう。

德田 和規
德田 和規
フロントエンド・エンジニア

HTML/CSSコーダーとしてWebに触れ、その後WebディレクターとしてJavaScriptの特性を活かしたサイトのテクニカルディレクションからUI開発までを経験。2011年にフロントエンド・エンジニアとして株式会社ピクセルグリッドへ入社。 著書に『jQuery逆引きマニュアル』(共著:インプレスジャパン、2010年12月17日)などがある。