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

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

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

この HTML を考えてください。

html
<input type="file" id="input" multiple />

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

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

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

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

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

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

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

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

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

js
const numFiles = fileList.length;

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

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

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

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

name

読み取り専用の文字列としてのファイル名。これはファイル名のみで、パスに関する情報は含まれていません。

size

読み取り専用の 64 ビット整数によるバイト単位のファイルサイズです。

type

読み取り専用の文字列としてのファイルの MIME タイプです。MIME タイプが特定できないときは空文字列 ("") となります。

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

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

html
<!doctype html>
<html lang="ja-JP">
  <head>
    <meta charset="UTF-8" />
    <title>ファイルのサイズ</title>
  </head>

  <body>
    <form name="uploadForm">
      <div>
        <input id="uploadInput" type="file" multiple />
        <label for="fileNum">選択されたファイル:</label>
        <output id="fileNum">0</output>;
        <label for="fileSize">合計サイズ:</label>
        <output id="fileSize">0</output>
      </div>
      <div><input type="submit" value="Send file" /></div>
    </form>

    <script>
      const uploadInput = document.getElementById("uploadInput");
      uploadInput.addEventListener(
        "change",
        () => {
          // 合計サイズを計算
          let numberOfBytes = 0;
          for (const file of uploadInput.files) {
            numberOfBytes += file.size;
          }

          // 最も近い接頭辞単位に近似
          const units = [
            "B",
            "KiB",
            "MiB",
            "GiB",
            "TiB",
            "PiB",
            "EiB",
            "ZiB",
            "YiB",
          ];
          const exponent = Math.min(
            Math.floor(Math.log(numberOfBytes) / Math.log(1024)),
            units.length - 1,
          );
          const approx = numberOfBytes / 1024 ** exponent;
          const output =
            exponent === 0
              ? `${numberOfBytes} bytes`
              : `${approx.toFixed(3)} ${
                  units[exponent]
                } (${numberOfBytes} bytes)`;

          document.getElementById("fileNum").textContent =
            uploadInput.files.length;
          document.getElementById("fileSize").textContent = output;
        },
        false,
      );
    </script>
  </body>
</html>

click() メソッドを使用して非表示の input 要素を使用する

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

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

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

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

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

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

<button> は、好きなようにスタイル付けできます。

label 要素を使用して非表示の file input 要素を起動

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

次の HTML を見てください。

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

そしてこの CSS です。

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

input.visually-hidden:is(:focus, :focus-within) + label {
  outline: thin dotted;
}

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

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

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

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

js
let dropbox;

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

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

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

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

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

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

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

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

  handleFiles(files);
}

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

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

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

js
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 = (e) => {
      img.src = e.target.result;
    };
    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 を作成します。

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

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

js
URL.revokeObjectURL(objectURL);

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

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

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

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() メソッドを使用して非表示の input 要素を使用するの節で説明されています。

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

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

fileSelect.addEventListener(
  "click",
  (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 = () => {
        URL.revokeObjectURL(img.src);
      };
      li.appendChild(img);
      const info = document.createElement("span");
      info.innerHTML = `${this.files[i].name}: ${this.files[i].size} バイト`;
      li.appendChild(info);
    }
  }
}

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

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

  1. 新しく順序なしリスト (<ul>) 要素を作成します。

  2. 新しいリスト要素は、<div> ブロックの中に Node.appendChild() メソッドを呼び出すことで挿入されます。

  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 にあり、対応する Filefile 属性に添付されていることを思い出してください。これにより、このようにDocument.querySelectorAll()を使用して、ユーザーがアップロードするために選択した画像をすべて選択することができます。

js
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 つの入力、画像要素と画像データを読み込むファイルを受け付けます。

js
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",
    (e) => {
      if (e.lengthComputable) {
        const percentage = Math.round((e.loaded * 100) / e.total);
        self.ctrl.update(percentage);
      }
    },
    false,
  );

  xhr.upload.addEventListener(
    "load",
    (e) => {
      self.ctrl.update(100);
      const canvas = self.ctrl.ctx.canvas;
      canvas.parentNode.removeChild(canvas);
    },
    false,
  );
  xhr.open(
    "POST",
    "https://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php",
  );
  xhr.overrideMimeType("text/plain; charset=x-user-defined-binary");
  reader.onload = (evt) => {
    xhr.send(evt.target.result);
  };
  reader.readAsBinaryString(file);
}

function createThrobber(img) {
  const throbberWidth = 64;
  const throbberHeight = 6;
  const throbber = document.createElement("canvas");
  throbber.classList.add("upload-progress");
  throbber.setAttribute("width", throbberWidth);
  throbber.setAttribute("height", throbberHeight);
  img.parentNode.appendChild(throbber);
  throbber.ctx = throbber.getContext("2d");
  throbber.ctx.fillStyle = "orange";
  throbber.update = (percent) => {
    throbber.ctx.fillRect(
      0,
      0,
      (throbberWidth * percent) / 100,
      throbberHeight,
    );
    if (percent === 100) {
      throbber.ctx.fillStyle = "green";
    }
  };
  throbber.update(0);
  return throbber;
}

上の 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
<?php
if (isset($_FILES['myFile'])) {
    // 例:
    move_uploaded_file($_FILES['myFile']['tmp_name'], "uploads/" . $_FILES['myFile']['name']);
    exit;
}
?><!DOCTYPE html>
<html lang="ja-JP">
<head>
  <meta charset="UTF-8">
  <title>dnd binary upload</title>
    <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 = () => {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    alert(xhr.responseText); // handle response.
                }
            };
            fd.append('myFile', file);
            // multipart/form-data のアップロードを開始します。
            xhr.send(fd);
        }

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

            dropzone.ondrop = (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;">ここにファイルをドラッグ & ドロップしてください</div>
    </div>
</body>
</html>

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

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

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

html
<iframe id="viewer"></iframe>

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

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

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

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

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

関連情報