File API入門 前編 File APIとFileReader APIの利用

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

発行

著者 德田 和規 テクニカルディレクター
File API入門 シリーズの記事一覧

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 Chrome Firefox Safari Opera IE iOS Android
File API 13+ 3.6+ 6+ 11.1+ 10+ 6+ 3+
FileReader API 13+ 3.6+ 6+ 11.1+ 10+ 6+ 3+
Blob constructing 20+ 13+ 6+ 12.1+ 10+ 6+ --
Blob URLs 8+ 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は配列ではなくオブジェクトですので、注意してください。

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

<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);

FileReader API

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

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

var reader = new FileReader();

FileReaderの読み込みメソッド

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

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

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

メソッド 引数 解説
readAsBinaryString Blob 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...

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

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

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

ファイルは複数選択できる仕様です。ファイル選択ダイアログが開いたら、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の幅が広がるでしょう。