ウェブアプリケーションからのファイルの使用
メモ: この機能はウェブワーカー内で利用可能です。
ファイル API を使用すると、ウェブコンテンツがユーザーにローカルファイルを選択するように指示し、それらのファイルを読み取ることができるようになりました。この選択は HTML の <input type="file"> 要素を使用したり、ドラッグ & ドロップを行ったりすることで行うことができます。
選択されたファイルへのアクセス
この HTML があったとします。
<input type="file" id="input" multiple />
ファイル API により、ユーザーが選択したファイルを表す File オブジェクトを含む FileList にアクセスすることができます。
multiple 属性を input 要素に付けることで、ユーザーが複数のファイルを選択することができるようになります。
旧来の 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 オブジェクトは、単に配列としてリストにアクセスするだけで取得できます。
File オブジェクトには 3 つのプロパティがあり、ファイルに関する有益な情報を得られます。
例: ファイルサイズを表示
次のコードは size プロパティを利用する例です。
<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>
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} バイト`
        : `${approx.toFixed(3)} ${units[exponent]} (${numberOfBytes} バイト)`;
    document.getElementById("fileNum").textContent = uploadInput.files.length;
    document.getElementById("fileSize").textContent = output;
  },
  false,
);
click() メソッドを使用して非表示の input 要素を使用する
見た目の悪い <input> 要素を非表示にし、独自のインターフェイスでファイル選択を開き、ユーザーが選択したファイルを表示することができます。 input 要素のスタイルを display:none とし、その上で click() メソッドを <input> に対して呼び出すことで実現できます。
次のような HTML を考えてみましょう。
<input type="file" id="fileElem" multiple accept="image/*" />
<button id="fileSelect" type="button">ファイルをいくつか選択</button>
#fileElem {
  display: none;
}
click イベントを扱うコードは次のようなものです。
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 を見てください。
<input
  type="file"
  id="fileElem"
  multiple
  accept="image/*"
  class="visually-hidden" />
<label for="fileElem">ファイルをいくつか選択</label>
そしてこの CSS です。
.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}
input.visually-hidden:is(:focus, :focus-within) + label {
  outline: thin dotted;
}
JavaScript コードを追加して fileElem.click() を呼び出す必要はありません。またこの場合は、ラベル要素のスタイルを希望どおりに設定することもできます。前例のようにアウトラインに設定したり、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 を持つ要素をドロップゾーンに指定しています。これは、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();
  const dt = e.dataTransfer;
  const files = dt.files;
  handleFiles(files);
}
ここでは、イベントから dataTransfer フィールドを取得し、そこからファイルリストを取得し、それを handleFiles() に渡します。これより先は、ユーザーが入力要素を使用したかドラッグ & ドロップを使用するかどうかにかかわらず、ファイルの処理方法は全く同じです。
例: ユーザーが選択した画像のサムネイルを表示
次の素晴らしい写真共有サイトを開発していて、ユーザーが実際に画像をアップロードする前に HTML を使って画像のサムネイルプレビューを表示させたいとしましょう。前に説明したように input 要素やドロップゾーンを設定し、次の handleFiles() のような関数を呼び出せば良いのです。
function handleFiles(files) {
  for (const file of files) {
    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 を作成します。
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/*" />
<a href="#" id="fileSelect">ファイルをいくつか選択</a>
<div id="fileList">
  <p>ファイルが選択されていません。</p>
</div>
#fileElem {
  display: none;
}
これにより、ファイル <input> 要素と、ファイル選択を呼び出すリンクが確立されます (あまり美しくないファイル入力を非表示にするため)。これは、ファイル選択を呼び出すメソッドと同様に、click() メソッドを使用して非表示の input 要素を使用するの節で説明されています。
handleFiles() メソッドは次のようになります。
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() {
  fileList.textContent = "";
  if (!this.files.length) {
    const p = document.createElement("p");
    p.textContent = "ファイルが選択されていません。";
    fileList.appendChild(p);
  } else {
    const list = document.createElement("ul");
    fileList.appendChild(list);
    for (const file of this.files) {
      const li = document.createElement("li");
      list.appendChild(li);
      const img = document.createElement("img");
      img.src = URL.createObjectURL(file);
      img.height = 60;
      li.appendChild(img);
      const info = document.createElement("span");
      info.textContent = `${file.name}: ${file.size} バイト`;
      li.appendChild(info);
    }
  }
}
これは、 <div> の URL を fileList という ID で取得することから始まります。これは、サムネイルを含むファイルリストを挿入するブロックです。
handleFiles() に渡された FileList オブジェクトが空の場合、ブロックの内部 HTML に「ファイルが選択されていません」と表示するように設定します。そうでない場合は、次のようにファイルリストの構築を開始します。
- 新しく順序なしリスト (<ul>) 要素を作成します。
- 新しいリスト要素は、<div>ブロックの中にNode.appendChild()メソッドを呼び出すことで挿入されます。
- filesで表される- FileList内のそれぞれの- Fileに対して次の処理を実行します。- 新しくリストアイテム (<li>) 要素を作成し、リストに挿入します。
- 新しく画像 (<img>) 要素を作成します。
- URL.createObjectURL()を用いて、Blob の URL を作成して、画像のソースをファイルを表す新しいオブジェクト URL に設定します。
- 画像の高さを 60 ピクセルに設定します。
- 新しいリストアイテムをリストに追加する。
 
- 新しくリストアイテム (
上のコードのライブデモはこちらです。
画像が読み込まれた直後にオブジェクト URL をすぐに取り消さないことに注意してください。そうすると、ユーザーが画像に対して操作(右クリックして画像を保存したり、新しいタブで開いたりなど)ができなくなってしまいます。長寿命のアプリケーションでは、オブジェクト URL が不要になった場合(画像が DOM から除去された場合など)に、 URL.revokeObjectURL() メソッドを呼び出し、オブジェクト URL 文字列を渡して、メモリーを解放するためにオブジェクト URL を無効にする必要があります。
例: ユーザーが選択したファイルを送信
この例では、ユーザーがファイル(例えば、前回の例で使用した選択した画像)をサーバーにアップロードする方法を示します。
メモ:
通常、 HTTP リクエストを行うためには、フェッチ API を XMLHttpRequest の代わりに使用することをお勧めします。ただし、この例では、ユーザーにアップロードの進行状況を表示したいのですが、この機能はフェッチ API ではまだ対応していないため、 XMLHttpRequest を使用しています。
フェッチ API を使用した進行状況の通知の標準化に関する取り組みは、 https://github.com/whatwg/fetch/issues/607 で行われています。
アップロードタスクの生成
前の例でサムネイルを作成したコードの続きで、すべてのサムネイル画像が CSS クラス obj にあり、対応する File が file 属性に添付されていることを思い出してください。これにより、このようにDocument.querySelectorAll()を使用して、ユーザーがアップロードするために選択した画像をすべて選択することができます。
function sendFiles() {
  const imgs = document.querySelectorAll(".obj");
  for (const img of imgs) {
    new FileUpload(img, img.file);
  }
}
document.querySelectorAll では CSS クラスが obj である文書中のすべての要素を取得します。この例では、これらの要素はすべての画像サムネイルになります。このリストを取得したら、それを参照して、それぞれの新しい FileUpload インスタンスを作成するのは簡単です。それぞれが対応するファイルのアップロードを処理します。
ファイルのアップロード処理を行う
FileUpload 関数は 2 つの入力、画像要素と画像データを読み込むファイルを受け付けます。
function FileUpload(img, file) {
  const reader = new FileReader();
  this.ctrl = createThrobber(img);
  const xhr = new XMLHttpRequest();
  this.xhr = xhr;
  this.xhr.upload.addEventListener(
    "progress",
    (e) => {
      if (e.lengthComputable) {
        const percentage = Math.round((e.loaded * 100) / e.total);
        this.ctrl.update(percentage);
      }
    },
    false,
  );
  xhr.upload.addEventListener(
    "load",
    (e) => {
      this.ctrl.update(100);
      const canvas = this.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 を作成します。
実際にデータを転送する前に、いくつかの準備段階があります。
- XMLHttpRequestのアップロード- progressリスナーは、アップロードの進捗に応じて最新の情報に基づいて throbber が更新されるように、新しいパーセント値の情報で throbber を更新するように設定されています。
- XMLHttpRequestのアップロード- loadイベントハンドラーは、進捗インジケーターが実際に 100 % に達することを確認するために、throbber の進捗情報を 100% に更新するように設定されています (プロセス中に粒度のクセがある場合)。そして、必要がなくなれば throbber を削除します。これにより、アップロードが完了すると throbber が消えます。
- 画像ファイルをアップロードするリクエストは、 XMLHttpRequestのopen()メソッドを呼び出して POST リクエストを生成することで開始されます。
- アップロードの MIME タイプは XMLHttpRequest関数のoverrideMimeType()を呼び出して設定します。この場合、一般的な MIME タイプを使用しています。用途によっては MIME タイプを設定する必要がない場合もあります。
- FileReaderオブジェクトを使用して、ファイルをバイナリー文字列に変換します
- 最後に、コンテンツがロードされると、 XMLHttpRequest関数のsend()が呼び出され、ファイルのコンテンツがアップロードされます。
ファイルのアップロード処理を非同期に処理する
この例では、サーバー側で PHP を使用し、クライアント側で JavaScript を使用して、ファイルの非同期アップロードを実演しています。
<?php
if (isset($_FILES["myFile"])) {
  // Example:
  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>
      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); // レスポンスを処理
          }
        };
        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 に設定する必要があります。
<iframe id="viewer"></iframe>
そして、src 属性の変更点はこちらです。
const objURL = URL.createObjectURL(blob);
const iframe = document.getElementById("viewer");
iframe.setAttribute("src", objURL);
// 後で:
URL.revokeObjectURL(objURL);
例: 他のファイル形式でのオブジェクト URL の使用
他の形式のファイルも同じように操作できます。ここでは、アップロードされた動画をプレビューする方法を紹介します。
const video = document.getElementById("video");
const objURL = URL.createObjectURL(blob);
video.src = objURL;
video.play();
// 後で:
URL.revokeObjectURL(objURL);