Web アプリケーションからファイルを扱う

HTML5 から DOM に追加された File API によって、Web ページがユーザに自身の環境下のファイルを要求し、その内容を読み込めるようになりました。ファイルの選択は HTML の <input> 要素もしくはドラッグ&ドロップから行えます。

File API を拡張や他の chrome コードから利用することもできます。この場合、もういくつか知っておきたい機能があります。詳細は Using the DOM File API in chrome code をご覧下さい。

HTML からファイルを選択する

File API を使ってなにか1つファイルを選択してみましょう。

<input type="file" id="input" onchange="handleFiles(this.files)">

ユーザがファイルを選択すると、handleFiles() 関数が呼び出されます。関数には FileList オブジェクトが与えられますが、このオブジェクトにはユーザが選択したファイルを表す File オブジェクトが格納されています。

複数のファイルを選択させたいときはただ、input 要素に multiple 属性を追加すればよいだけです。

<input type="file" id="input" multiple onchange="handleFiles(this.files)">

この場合、handleFiles() 関数に渡されるファイルリストには、ユーザが選択した各ファイルに対応する File オブジェクトが格納されています。

click() メソッドを使い input 要素を隠す

Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) から、ファイル選択用の <input> 要素を隠し、あなた好みの見た目にしたファイル選択用インターフェースを用意することができるようになりました。input 要素のスタイルを display: none とし、その上で click() メソッドを呼び出せばよいのです。

次のような HTML を考えてみましょう。

<input type="file" id="fileElem" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)">
<a href="#" id="fileSelect">ファイルを選択</a>

これに次のようなスクリプトを書けば、リンクをクリックしてファイルピッカーを呼び出せます。

var fileSelect = document.getElementById("fileSelect"),
    fileElem   = document.getElementById("fileElem");

fileSelect.addEventListener("click", function (e) {
  if (fileElem) {
    fileElem.click();
  }
  e.preventDefault(); // "#" に移動するのを防ぐ
}, false);

もちろん、リンクではなくボタンなどにして、スタイルを思うようにカスタマイズできます。

動的に change イベントリスナを登録する

もし input 要素が jQuery のような JavaScript ライブラリによって生成されている場合は、 element.addEventListener() を使用し change イベントリスナを登録する必要があります。

var inputElement = document.getElementById("inputField");
inputElement.addEventListener("change", handleFiles, false);

function handleFiles() {
  var fileList = this.files;

  /* ファイルリストを処理するコードがここに入る */
}

この場合、handleFiles() 関数は引数を取るのではなく、ファイルリストを探すことに注意してください。このようにして登録されたイベントリスナには、引数を与えられないのです。

オブジェクト URL を利用する

Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) では、 window.URL.createObjectURL() メソッドと window.URL.revokeObjectURL() メソッドがサポートされました。これらのメソッドを使えば、 File オブジェクトをシンプルな URL として参照できます。ユーザのコンピュータにあるファイルも例外ではありません。

HTML ファイルから URL で参照したい File オブジェクトがある場合、そのオブジェクト URL は次のように作成できます。

var objectURL = window.URL.createObjectURL(fileObj);

オブジェクト URL は File オブジェクトを識別する文字列です。window.URL.createObjectURL() メソッドを呼び出すたびに、一意なオブジェクト URL が生成されます。これはたとえ既に同じファイルについてオブジェクト URL を生成していたとしてもです。

オブジェクト URL はドキュメントが解放された際に自動的に解放されますが、もしあなたのページが動的にオブジェクト URL を扱う場合は、window.URL.revokeObjectURL() メソッドを使い明示的に開放する方がよいでしょう。

window.URL.revokeObjectURL(objectURL);

ドラッグ&ドロップでファイルを選択する

ドラッグ&ドロップでファイルを読みこませることもできます。

これを実装するにあたって最初にすることは、ドロップ領域の生成です。どの部分がファイルのドロップを受け付けるかは Web アプリケーションのデザインによりますが、ある要素がドロップイベントを受け付けるようにするのは簡単です。

var dropbox;

dropbox = document.getElementById("dropbox");
dropbox.addEventListener("dragenter", dragenter, false);
dropbox.addEventListener("dragover", dragover, false);
dropbox.addEventListener("drop", drop, false);

この例では、iddropbox を持つ要素をドロップ領域としました。要素をドロップ領域にするには、dragenter, dragover, drop イベントのリスナを登録すればよいのです。

この例で dragenter, dragover イベントについて何かする必要はとくにありません。ですので渡す関数はとてもシンプルです。ただイベントの伝搬を停止し、規定のアクションが起こらないようにしているだけです。

function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}

重要なのは drop() 関数です。

function drop(e) {
  e.stopPropagation();
  e.preventDefault();

  var dt = e.dataTransfer;
  var files = dt.files;

  handleFiles(files);
}

ここでイベントから dataTransfer フィールドを受け取り、そこからファイルリストを取得して handleFiles()に渡しています。ここからのファイル操作は input 要素を使うのとまったく同じです。

ファイルの情報を得る

FileList オブジェクトはユーザが選択したファイルを格納しています。それぞれのファイルは File オブジェクトとして表現されています。ユーザがいくつファイルを選択したかは、ファイルリストの length プロパティから取得できます。

var numFiles = files.length;

個々の File オブジェクトは、ファイルリストに配列のようにアクセスすることで取得できます。

for (var i = 0, numFiles = files.length; i < numFiles; i++) {
  var file = files[i];
  ...
}

このループはファイルリスト中のすべてのファイルにアクセスします。

File オブジェクトには3つのプロパティが用意されており、ファイルに関する有益な情報を得られます。

name
ファイル名 (読み取り専用)。このプロパティはファイル名のみを持ち、パスに関する情報は何も持ちあわせていません。
size
ファイルサイズ (読み取り専用)。64ビット整数のバイトで表現されています。
type
ファイルの MIME タイプ (読み取り専用)。MIME タイプが決定できないときは空文字列 ("") を返します。

例: ファイルサイズを表示

次のコードは size プロパティを利用する例です。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File(s) size</title>
<script>
function updateSize() {
  var nBytes = 0,
      oFiles = document.getElementById("uploadInput").files,
      nFiles = oFiles.length;
  for (var nFileId = 0; nFileId < nFiles; nFileId++) {
    nBytes += oFiles[nFileId].size;
  }
  var sOutput = nBytes + " bytes";
  // optional code for multiples approximation
  for (var aMultiples = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"], nMultiple = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, nMultiple++) {
    sOutput = nApprox.toFixed(3) + " " + aMultiples[nMultiple] + " (" + nBytes + " bytes)";
  }
  // end of optional code
  document.getElementById("fileNum").innerHTML = nFiles;
  document.getElementById("fileSize").innerHTML = sOutput;
}
</script>
</head>

<body onload="updateSize();">
<form name="uploadForm">
<p><input id="uploadInput" type="file" name="myFiles" onchange="updateSize();" multiple> selected files: <span id="fileNum">0</span>; total size: <span id="fileSize">0</span></p>
<p><input type="submit" value="Send file"></p>
</form>
</body>
</html>

例: ユーザが選択した画像のサムネイルを表示

あなたはフォト共有サイトをつくっており、ユーザがアップロードする写真を選んでいるときに、そのサムネイル一覧をアップロードすることなしにプレビューさせたいとします。これを実現するのも簡単で、input 要素もしくはドロップ領域を用意してから、次のような handleFiles() 関数を呼べば良いのです。

function handleFiles(files) {
  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    var imageType = /image.*/;
    
    if (!file.type.match(imageType)) {
      continue;
    }
    
    var img = document.createElement("img");
    img.classList.add("obj");
    img.file = file;
    preview.appendChild(img);
    
    var reader = new FileReader();
    reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
    reader.readAsDataURL(file);
  }
}

ユーザが選択したファイルを処理するループで、ファイルの type 属性を見てそれが画像なのかを確かめています (正規表現で /image.*/ にマッチするかを調べています)。画像だと分かったファイルについて新しい img 要素を生成します。画像のサイズを指定したり、ボーダーをつけたり、ドロップシャドウを与えたりするのには CSS を使えばよいので、このコードでする必要は特にありません。

img 要素には obj という class が追加され、DOM ツリーから探しやすくなります。また、file という属性を設け、そこに画像の File オブジェクトを指定します。これにより、あとで実際にアップロードする画像を保持しておけるのです。最後に、 appendChild() で新しいサムネイルを文書のプレビュー領域に追加します。

そして、画像を FileReader で非同期に読み込み、img 要素に紐付けます。新しい FileReader オブジェクトを生成し、onload 関数をセットアップします。そして、readAsDataURL() を呼び、ファイル読み込みをバックグラウンドで開始します。画像ファイルの内容がすべて読み込まれると、それらは data: URL に変換され、onload のコールバックに渡されます。ここでの実装はただ img 要素の src 属性に読み込まれた画像をセットしているだけです。これでユーザの画面上にサムネイルを表示させられます。

例: オブジェクト URL で画像を表示

この例ではオブジェクト URL を使って画像のサムネイルを表示します。さらに、ファイル名やファイルサイズも表示します。サンプルもあります。

インターフェースとなる HTML は次のようになりました。

<input type="file" id="fileElem" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)">
<a href="#" id="fileSelect">Select some files</a> 
<div id="fileList">
  <p>No files selected!</p>
</div>

まずファイルを読み出す input 要素をつくり、ファイルピッカーを呼び出すリンクもつくります。あまり見栄えのよくない input 要素の UI を隠したいからです。これは先のセクションで紹介したものと同じです。最後に、サムネイルを表示する領域を <div> 要素で作ります。各要素の id 属性はそれぞれ fileElem, fileSelect, fileList としました。

handleFiles()メソッドはこんな風になります。

window.URL = window.URL || window.webkitURL;

var fileSelect = document.getElementById("fileSelect"),
    fileElem   = document.getElementById("fileElem"),
    fileList   = document.getElementById("fileList");

fileSelect.addEventListener("click", function (e) {
  if (fileElem) {
    fileElem.click();
  }
  e.preventDefault(); // prevent navigation to "#"
}, false);

function handleFiles(files) {
  if (!files.length) {
    fileList.innerHTML = "<p>No files selected!</p>";
  } else {
    var list = document.createElement("ul");
    for (var i = 0; i < files.length; i++) {
      var li = document.createElement("li");
      list.appendChild(li);
      
      var img = document.createElement("img");
      img.src = window.URL.createObjectURL(files[i]);
      img.height = 60;
      img.onload = function(e) {
        window.URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      
      var info = document.createElement("span");
      info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
      li.appendChild(info);
    }
    fileList.appendChild(list);
  }
}

まず、前述した3つの要素を取得します。

ファイルが選択されたときに、handleFiles() に渡された FileListnull だった場合、"No files selected!" というテキストを書き込んでいます。null でない場合は、次のステップに従い、fileList 内にサムネイルを書き込みます。

  1. 新しく <ul> 要素を作成する
  2. element.appendChild() メソッドを使用し、その ul 要素を fileList に追加する
  3. FileList オブジェクト (files) 中の各 File オブジェクトについて以下を実行する
    1. 新しく <li> 要素を生成し、さきほどの ul 要素に追加する
    2. 新しく <img> 要素を生成する
    3. window.URL.createObjectURL() でファイルのオブジェクト URL を作成し、img 要素の src 属性に指定する
    4. 画像の高さを60ピクセルに指定する
    5. 画像の load イベントハンドラを追加し、画像が読み込まれたら window.URL.revokeObjectURL()img.src で与えたオブジェクトを渡し、オブジェクト URL を解放するようにする (画像が読み込まれたら必要なくなるため)
    6. li 要素を ul 要素に追加する

例: ユーザが選択したファイルを送信

ひとつ前の画像サムネイルの例のように、ユーザが選択したファイルをサーバへ送信したい場合もあるでしょう。これもとても簡単に、そして非同期に行うことができます。

アップロードタスクの生成

では、さきほどのサムネイルを生成する例を拡張しましょう。さきほどの例では、サムネイル画像には obj という class がつけられており、またそれぞれの File オブジェクトは file という属性につけられていました。これにより、ユーザがアップロードのため選択した画像を得るのはとても簡単です。 document.querySelectorAll() を使うと、次のように書けます。

function sendFiles() {
  var imgs = document.querySelectorAll(".obj");
  
  for (var i = 0; i < imgs.length; i++) {
    new FileUpload(imgs[i], imgs[i].file);
  }
}

2行目で、imgs という変数に、obj という class がつけられた要素のリストを格納しています。この場合、要素はすべて画像サムネイルになります。要素のリストを得られたら後は簡単です。リストを見ていき、ひとつのアイテムに対し FileUpload インスタンスを生成すれば良いのです。それぞれのハンドラが該当するファイルをアップロードします。

ファイルのアップロードプロセス処理

FileUpload 関数は2つの引数を取ります。1番目は img 要素で、2番目が画像データを読むファイルになります。

function FileUpload(img, file) {
  var reader = new FileReader();  
  this.ctrl = createThrobber(img);
  var xhr = new XMLHttpRequest();
  this.xhr = xhr;
  
  var self = this;
  this.xhr.upload.addEventListener("progress", function(e) {
        if (e.lengthComputable) {
          var percentage = Math.round((e.loaded * 100) / e.total);
          self.ctrl.update(percentage);
        }
      }, false);
  
  xhr.upload.addEventListener("load", function(e){
          self.ctrl.update(100);
          var canvas = self.ctrl.ctx.canvas;
          canvas.parentNode.removeChild(canvas);
      }, false);
  xhr.open("POST", "http://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php");
  xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
  reader.onload = function(evt) {
    xhr.send(evt.target.result);
  };
  reader.readAsBinaryString(file);
}

FileUpload() 関数は throbber という、進行状況を表示するものを作ります。その後、 XMLHttpRequest でデータをアップロードします。

データを実際に転送する前には、次のようなステップが実行されています。

  1. XMLHttpRequest のアップロード progress リスナが登録され、throbber に新しいパーセント値を設定します。こうすることでアップロードが進行しても、throbber が最新の状態を反映します
  2. XMLHttpRequest のアップロード load イベントハンドラが登録され、throbber を100%に更新します (これは進行状況の表示がちゃんと100%になるように見せるためです)。そして、もう必要がなくなった throbber を削除します。つまりアップロードが終わると、throbber が消えるということです
  3. 画像ファイルのアップロードリクエストは XMLHttpRequestopen() メソッドを呼び出し、POST リクエストを開始させます
  4. アップロードの MIME タイプは XMLHttpRequestoverrideMimeType() メソッドで設定します。この場合は一般的な MIME タイプを設定しています。何をするかによりますが、MIME タイプを指定しなくてもいい場合もあります
  5. ファイルをバイナリ文字列に変換するため、FileReader オブジェクトを使用します
  6. 最後に、内容が読み込まれたら XMLHttpRequestsendAsBinary() メソッドが呼び出され、ファイルをアップロードします

Handling the upload process for a file, asynchronously (deprecated getAsBinary)

function fileUpload(file) {
  // Please report improvements to: marco.buratto at tiscali.it
  
  var fileName = file.name,
    fileSize = file.size,
    fileData = file.getAsBinary(), // works on TEXT data ONLY. use new FileReader
    boundary = "xxxxxxxxx",
    uri = "serverLogic.php",
    xhr = new XMLHttpRequest();
  
  xhr.open("POST", uri, true);
  xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary="+boundary); // simulate a file MIME POST request.
  xhr.setRequestHeader("Content-Length", fileSize);
  
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if ((xhr.status >= 200 && xhr.status <= 200) || xhr.status == 304) {
        
        if (xhr.responseText != "") {
          alert(xhr.responseText); // display response.
        }
      }
    }
  }
  
  var body = "--" + boundary + "\r\n";
  body += "Content-Disposition: form-data; name='fileId'; filename='" + fileName + "'\r\n";
  body += "Content-Type: application/octet-stream\r\n\r\n";
  body += fileData + "\r\n";
  body += "--" + boundary + "--";
  
  xhr.send(body);
  return true;
}

This needs to be modified for working with binary data, too.

See also

ドキュメントのタグと貢献者

 このページの貢献者: ethertank, myakura, saneyuki_s, souta
 最終更新者: ethertank,