Web アプリケーションからのファイルの使用

HTML5 から DOM に追加された File API によって、ウェブコンテンツがユーザーにローカルファイルを選択するように指示し、それらのファイルを読み取れるようになりました。この選択は HTML の <input type="file"> 要素か、ドラッグ&ドロップのどちらかを使用することで行うことができます。

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

選択されたファイルへのアクセス

この HTML を見てください。

<input type="file" id="input">

File API では、ユーザーが選択したファイルを表す File オブジェクトを含む FileList にアクセスすることができます。

input 要素の multiple 属性により、複数のファイルを選択することができます。

旧来の DOM セレクターを使って、最初に選択されたファイルにアクセスします。

const selectedFile = document.getElementById('input').files[0];

change イベントでの選択されたファイルへのアクセス

change イベントを通して FileList にアクセスすることも可能です (ただし必須ではありません)。このように EventTarget.addEventListener() を使って change イベントのリスナーを追加する必要があります。

const inputElement = document.getElementById("input");
inputElement.addEventListener("change", handleFiles, false);
function handleFiles() {
  const fileList = this.files; /* ファイルリストを処理するコードがここに入る */
}

選択されたファイルについての情報の取得

DOM が提供する FileList オブジェクトは、File オブジェクトとして指定された、ユーザーが選択したすべてのファイルをリストアップします。ファイルリストの length 属性の値をチェックすることで、ユーザーが選択したファイルの数を知ることができます。

const numFiles = fileList.length;

個々の File オブジェクトは、単に配列としてリストにアクセスするだけで取得できます。

for (let i = 0, numFiles = fileList.length; i < numFiles; i++) {
  const file = fileList[i];
  // ...
}

このループは、ファイルリスト内のすべてのファイルを繰り返し処理します。

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

name
読み取り専用の文字列としてのファイル名。これはファイル名のみで、パスに関する情報は含まれていません。
size
読み取り専用の64ビット整数によるバイト単位のファイルサイズです。
type
読み取り専用の文字列としてのファイルの MIME タイプ (読み取り専用)。MIME タイプが特定できないときは空文字列 ("") となります。

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

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

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>File(s) size</title>
</head>

<body>
  <form name="uploadForm">
    <div>
      <input id="uploadInput" type="file" name="myFiles" multiple>
      選択されたファイル: <span id="fileNum">0</span>;
      合計サイズ: <span id="fileSize">0</span>
    </div>
    <div><input type="submit" value="Send file"></div>
  </form>
 
  <script>
  function updateSize() {
    let nBytes = 0,
        oFiles = this.files,
        nFiles = oFiles.length;
    for (let nFileId = 0; nFileId < nFiles; nFileId++) {
      nBytes += oFiles[nFileId].size;
    }
    let sOutput = nBytes + " bytes";
    // 倍数近似のための任意のコード
    const aMultiples = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
    for (nMultiple = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, nMultiple++) {
      sOutput = nApprox.toFixed(3) + " " + aMultiples[nMultiple] + " (" + nBytes + " bytes)";
    }
    // 任意コードの末尾
    document.getElementById("fileNum").innerHTML = nFiles;
    document.getElementById("fileSize").innerHTML = sOutput;
  }

  document.getElementById("uploadInput").addEventListener("change", updateSize, false);
  </script>
</body>
</html>

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

見た目の悪い <input> 要素を隠し、独自のインターフェイスでファイル選択を開き、ユーザーが選択したファイルを表示することができます。 input 要素のスタイルを display: none とし、その上で click() メソッドを <input> に対して呼び出すことで実現できます。

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

<input type="file" id="fileElem" multiple accept="image/*" style="display:none">
<button id="fileSelect">いくつかのファイルを選択します。</button>

click イベントを扱うコードは次のようなものです。

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

fileSelect.addEventListener("click", function (e) {
  if (fileElem) {
    fileElem.click();
  }
}, false);

ファイル選択を開く新しいボタンは、好きなようにスタイル付けできます。

label 要素を使用して隠した file input 要素を起動

JavaScript (click() メソッド) を使用せずにファイルピッカーを開けるようにするために、 <label> 要素を使用します。この場合、もし入力エレメントの display: none (または visibility: hidden) を設定して非表示に設定すると、ラベルがキーボードからアクセスできなくなります。代わりに、視覚的に非表示にする手法 (visually-hidden technique) を使用します。

次の HTMLを見てください。

<input type="file" id="fileElem" multiple accept="image/*" class="visually-hidden">
<label for="fileElem">いくつかのファイルを選択します。</label>

そしてこの CSS です。

.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 1px);
}

/* 互換性のための別ルールとして、最近の Firefox と Chrome では :focus-within が必要です。 */
input.visually-hidden:focus + label {
  outline: thin dotted;
}
input.visually-hidden:focus-within + label {
  outline: thin dotted;
}

fileElem.click()を呼び出すための JavaScript コードを追加する必要はありません。またこの場合は、ラベル要素のスタイルを希望どおりに設定することもできます。前例のようにアウトラインに設定したり、background-color、box-shadow を設定したりして、ラベルの非表示入力フィールドのフォーカスステータスを視覚的に示す必要があります。(この記事を書いている時点では、Firefox は<input type="file"> 要素に対してこの視覚的な手がかりを表示していません)

ドラッグ & ドロップを使用したファイルの選択

ユーザーがファイルをウェブアプリケーションにドラッグ & ドロップすることもできます。

最初のステップは、ドロップゾーンを確立することです。コンテンツのどの部分がドロップを受け入れるかは、アプリケーションの設計によって異なりますが、要素をドロップイベントを受け取るのは簡単です。

let dropbox;

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

この例では、ID dropbox を持つ要素をドロップゾーンに指定しています。これは、dragenterdragover、および drop イベントのリスナーを追加することで行われます。

実際には、この場合、dragenterdragoverのイベントでは何もする必要はありませんので、これらの関数はどちらも簡単です。これらの関数はイベントの伝播を停止し、デフォルトのアクションが発生しないようにするだけです。

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

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

本当の魔術は drop() 関数の中で起こります。

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

  const dt = e.dataTransfer;
  const files = dt.files;

  handleFiles(files);
}

ここでは、イベントから dataTransfer フィールドを取得し、そこからファイルリストを取得し、それを handleFiles() に渡します。これより先は、ユーザーが入力要素を使用したかドラッグ & ドロップを使用するかどうかにかかわらず、ファイルの処理方法は全く同じです。

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

次の素晴らしい写真共有サイトを開発していて、ユーザーが実際に画像をアップロードする前に HTML を使って画像のサムネイルプレビューを表示させたいとしましょう。前に説明したように input 要素やドロップゾーンを設定し、次の handleFiles() のような関数を呼び出せば良いのです。

function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    
    if (!file.type.startsWith('image/')){ continue }
    
    const img = document.createElement("img");
    img.classList.add("obj");
    img.file = file;
    preview.appendChild(img); // 「プレビュー」とは、コンテンツが表示される div 出力のことを想定しています。
    
    const reader = new FileReader();
    reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img);
    reader.readAsDataURL(file);
  }
}

ここでは、ユーザーが選択したファイルを処理するループが各ファイルの type 属性を見て、その MIME タイプが文字列 "image/" で始まるかどうかを確認しています)。画像である各ファイルに対して、新しい img 要素を作成します。CSS は、きれいな境界線や影を設定したり、画像のサイズを指定したりするために使用しますので、ここでは必要ありません。

各画像には CSS クラス obj が追加されており、DOM ツリーで簡単に見つけることができます。また、各画像に file 属性を追加し、画像の File を指定しています。これにより、後で実際にアップロードする画像を取得することができます。Node.appendChild() を使用して、ドキュメントのプレビュー領域に新しいサムネイルを追加します。

次に、画像の読み込みと img 要素へのアタッチを非同期で処理するための FileReader を確立します。新しい FileReader オブジェクトを作成した後、その onload 関数を設定し、readAsDataURL() を呼び出してバックグラウンドで読み込み処理を開始します。画像ファイルのコンテンツ全体が読み込まれると、それらは data: URL に変換され、onload コールバックに渡されます。このルーチンの実装では、img 要素の src 属性が読み込まれた画像に設定され、その結果、画像がユーザの画面のサムネイルに表示されます。

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

DOM URL.createObjectURL()URL.revokeObjectURL() メソッドを使用すると、ユーザーのコンピューター上のローカルファイルなど、DOM File オブジェクトを使用して参照可能なあらゆるデータを参照するために使用できるシンプルな URL 文字列を作成できます。

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

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

オブジェクト URL は File オブジェクトを識別する文字列です。 URL.createObjectURL() を呼び出すたびに、すでにそのファイルのオブジェクト URL を作成していても、一意のオブジェクト URL が作成されます。これらはそれぞれ解除する必要があります。これらはドキュメントがアンロードされると自動的に解放されますが、ページが動的にこれらを使用している場合は URL.revokeObjectURL() を呼び出して明示的に解放する必要があります。

URL.revokeObjectURL(objectURL);

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

この例では、オブジェクト URL を使用して画像のサムネイルを表示しています。さらに、ファイル名やサイズなどの他のファイル情報も表示します。

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

<input type="file" id="fileElem" multiple accept="image/*" style="display:none">
<a href="#" id="fileSelect">いくつかのファイルを選択します。</a> 
<div id="fileList">
  <p>選択されたファイルはありません!</p>
</div>

これにより、ファイル <input> 要素と、ファイル ピッカーを呼び出すリンクが確立されます (あまり美しくないファイル入力を非表示にするため)。これは、ファイル ピッカーを呼び出すメソッドと同様に、セクション click() メソッドを使用して非表示のファイル入力要素を使用する で説明されています。

handleFiles() メソッドは次のようになります。

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

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

fileElem.addEventListener("change", handleFiles, false); 

function handleFiles() {
  if (!this.files.length) {
    fileList.innerHTML = "<p>ファイルが選択されていません!</p>";
  } else {
    fileList.innerHTML = "";
    const list = document.createElement("ul");
    fileList.appendChild(list);
    for (let i = 0; i < this.files.length; i++) {
      const li = document.createElement("li");
      list.appendChild(li);
      
      const img = document.createElement("img");
      img.src = URL.createObjectURL(this.files[i]);
      img.height = 60;
      img.onload = function() {
        URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      const info = document.createElement("span");
      info.innerHTML = this.files[i].name + ": " + this.files[i].size + " bytes";
      li.appendChild(info);
    }
  }
}

これは、<div> の URL を fileList という ID で取得することから始まります。これは、サムネイルを含むファイルリストを挿入するブロックです。

handleFiles() に渡された FileList オブジェクトが null の場合、ブロックの内部 HTML に「ファイルが選択されていません」と表示するように設定します。そうでない場合は、次のようにファイルリストの構築を開始します。

  1. 新しく順序なしリスト (<ul>) 要素を作成します
  2. 新しいリスト要素は、<div> ブロックの Node.appendChild() メソッドを呼び出して <div> ブロックに挿入されます
  3. files で表される FileList 内の各 File に対して次の処理を実行します
    1. 新しくリスト項目 (<li>) 要素を作成し、リストに挿入します
    2. 新しく画像 (<img>) 要素を作成します
    3. URL.createObjectURL() を用いて、Blob の URL を作成して、画像のソースをファイルを表す新しいオブジェクト URL に設定します
    4. 画像の高さを60ピクセルに設定します
    5. 画像が読み込まれると不要になるため、画像の読み込みイベントハンドラを設定してオブジェクトの URL を解放します。これは URL.revokeObjectURL() メソッドを呼び出し、img.src で指定したオブジェクト URL 文字列を渡すことで行います
    6. 新しいリスト項目をリストに追加する

上のコードのライブデモはこちらです。

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

もう1つは、ユーザーが選択したファイルやファイル (先ほどの例で選択した画像など) をサーバーにアップロードできるようにすることです。これは非常に簡単に非同期で行うことができます。

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

前の例でサムネイルを作成したコードの続きで、すべてのサムネイル画像が CSS クラス obj にあり、対応する File} が file 属性に添付されていることを思い出してください。これにより、このようにDocument.querySelectorAll()を使用して、ユーザーがアップロードするために選択した画像をすべて選択することができます。

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

2 行目は、CSS クラス obj を持つドキュメント内のすべての要素の NodeList} を取得し imgs と呼ばれる変数に格納します。この例では、これらの要素はすべての画像サムネイルになります。このリストを取得したら、それを参照して、それぞれの新しい FileUpload インスタンスを作成するのは簡単です。それぞれが対応するファイルのアップロードを処理します。

ファイルのアップロード処理を行う

FileUpload 関数は2つの入力、画像要素と画像データを読み込むファイルを受け付けます。

function FileUpload(img, file) {
  const reader = new FileReader();  
  this.ctrl = createThrobber(img);
  const xhr = new XMLHttpRequest();
  this.xhr = xhr;
  
  const self = this;
  this.xhr.upload.addEventListener("progress", function(e) {
        if (e.lengthComputable) {
          const percentage = Math.round((e.loaded * 100) / e.total);
          self.ctrl.update(percentage);
        }
      }, false);
  
  xhr.upload.addEventListener("load", function(e){
          self.ctrl.update(100);
          const 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 イベントハンドラは、進捗インジケータが実際に 100 % に達することを確認するために、throbber の進捗情報を 100 % に更新するように設定されています (プロセス中に粒度のクセがある場合)。そして、必要がなくなれば throbber を削除します。これにより、アップロードが完了すると throbber が消えます
  3. 画像ファイルをアップロードするリクエストは、XMLHttpRequestopen() メソッドを呼び出して POST リクエストを生成することで開始されます
  4. アップロードの MIME タイプは XMLHttpRequest 関数の overrideMimeType() を呼び出して設定します。この場合、一般的な MIME タイプを使用しています。ユースケースによっては MIME タイプを設定する必要がない場合もあります
  5. FileReader オブジェクトを使用して、ファイルをバイナリ文字列に変換します
  6. 最後に、コンテンツがロードされると、XMLHttpRequest 関数の send() が呼び出され、ファイルのコンテンツがアップロードされます

ファイルのアップロード処理を非同期に扱う

この例では、サーバー側で PHP を使用し、クライアント側で JavaScript を使用して、ファイルの非同期アップロードを実演しています。

<?php
if (isset($_FILES['myFile'])) {
    // 例:
    move_uploaded_file($_FILES['myFile']['tmp_name'], "uploads/" . $_FILES['myFile']['name']);
    exit;
}
?><!DOCTYPE html>
<html>
<head>
    <title>dnd binary upload</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="application/javascript">
        function sendFile(file) {
            const uri = "/index.php";
            const xhr = new XMLHttpRequest();
            const fd = new FormData();
            
            xhr.open("POST", uri, true);
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    alert(xhr.responseText); // handle response.
                }
            };
            fd.append('myFile', file);
            // multipart/form-data のアップロードを開始します。
            xhr.send(fd);
        }

        window.onload = function() {
            const dropzone = document.getElementById("dropzone");
            dropzone.ondragover = dropzone.ondragenter = function(event) {
                event.stopPropagation();
                event.preventDefault();
            }
    
            dropzone.ondrop = function(event) {
                event.stopPropagation();
                event.preventDefault();

                const filesArray = event.dataTransfer.files;
                for (let i=0; i<filesArray.length; i++) {
                    sendFile(filesArray[i]);
                }
            }
        }
    </script>
</head>
<body>
    <div>
        <div id="dropzone" style="margin:30px; width:500px; height:300px; border:1px dotted grey;">Drag & drop your file here...</div>
    </div>
</body>
</html>

例: オブジェクト URL を使用して PDF を表示

オブジェクト URL は画像以外にも使用できます。埋め込まれた PDF ファイルや、ブラウザーで表示可能な他のリソースを表示するために使用できます。

Firefox では、 PDF が iframe 内に埋め込まれて表示されるようにするには (ダウンロードファイルとして提案されるのではなく)、pdfjs.disabled の設定を false に設定する必要があります。

<iframe id="viewer">

そして、src 属性の変更点はこちらです。

const obj_url = URL.createObjectURL(blob);
const iframe = document.getElementById('viewer');
iframe.setAttribute('src', obj_url);
URL.revokeObjectURL(obj_url);

例: 他のファイル形式でのオブジェクト URL の使用

他の形式のファイルも同じように操作できます。ここでは、アップロードされた動画をプレビューする方法を紹介します。

const video = document.getElementById('video');
const obj_url = URL.createObjectURL(blob);
video.src = obj_url;
video.play();
URL.revokeObjectURL(obj_url);

仕様書

仕様書 状態 備考
HTML Living Standard
File upload state の定義
現行の標準
File API 草案 初回定義

関連情報