ウェブアプリケーションからのファイルの使用
メモ: この機能はウェブワーカー内で利用可能です。
ファイル API を使用すると、ウェブコンテンツがユーザーにローカルファイルを選択するように指示し、それらのファイルを読み取ることができるようになりました。この選択は HTML の<input type="file">
要素を使用したり、ドラッグ & ドロップを行ったりすることで行うことができます。
選択されたファイルへのアクセス
change イベントでの選択されたファイルへのアクセス
change
イベントを通してFileList
にアクセスすることもできます (ただし必須ではありません)。このようにEventTarget.addEventListener()
を使ってchange
イベントのリスナーを追加する必要があります。
const inputElement = document.getElementById("input");inputElement.addEventListener("change", handleFiles, false);function handleFiles() { const fileList = this.files; /* ファイルリストを処理するコードがここに入る */}
選択されたファイルについての情報の取得
例: ファイルサイズを表示
次のコードはsize
プロパティを利用する例です。
<!doctype html><html lang="ja-JP"> <head> <meta charset="UTF-8" /> <title>ファイルのサイズ</title> </head> <body> <form name="uploadForm"> <div> <input type="file" multiple /> <label for="fileNum">選択されたファイル:</label> <output>0</output>; <label for="fileSize">合計サイズ:</label> <output>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 を考えてみましょう。
<input type="file" multiple accept="image/*" /><button type="button"> いくつかのファイルを選択してください。</button>
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" multiple accept="image/*" /><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);
この例では、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(); 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 = (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()
メソッドを使用すると、ユーザーのコンピューター上のローカルファイルなど、DOMFile
オブジェクトを使用して参照可能なあらゆるデータを参照するために使用できるシンプルな 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" multiple accept="image/*" /><a href="#">いくつかのファイルを選択してください。</a><div> <p>ファイルが選択されていません。</p></div>
これにより、ファイル<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 (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; li.appendChild(img); const info = document.createElement("span"); info.textContent = `${this.files[i].name}: ${this.files[i].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 (let i = 0; i < imgs.length; i++) { new FileUpload(imgs[i], imgs[i].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; 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
を作成します。
実際にデータを転送する前に、いくつかの準備段階があります。
XMLHttpRequest
のアップロードprogress
リスナーは、アップロードの進捗に応じて最新の情報に基づいて throbber が更新されるように、新しいパーセント値の情報で throbber を更新するように設定されています。XMLHttpRequest
のアップロードload
イベントハンドラーは、進捗インジケーターが実際に 100 % に達することを確認するために、throbber の進捗情報を 100% に更新するように設定されています (プロセス中に粒度のクセがある場合)。そして、必要がなくなれば throbber を削除します。これにより、アップロードが完了すると throbber が消えます。- 画像ファイルをアップロードするリクエストは、
XMLHttpRequest
のopen()
メソッドを呼び出して POST リクエストを生成することで開始されます。 - アップロードの MIME タイプは
XMLHttpRequest
関数のoverrideMimeType()
を呼び出して設定します。この場合、一般的な MIME タイプを使用しています。用途によっては MIME タイプを設定する必要がない場合もあります。 FileReader
オブジェクトを使用して、ファイルをバイナリー文字列に変換します- 最後に、コンテンツがロードされると、
XMLHttpRequest
関数のsend()
が呼び出され、ファイルのコンテンツがアップロードされます。
ファイルのアップロード処理を非同期に処理する
この例では、サーバー側で PHP を使用し、クライアント側で JavaScript を使用して、ファイルの非同期アップロードを実演しています。
<?phpif (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 > ここにファイルをドラッグ & ドロップしてください </div> </div> </body></html>
例: オブジェクト URL を使用して PDF を表示
オブジェクト URL は画像以外にも使用できます。埋め込まれた PDF ファイルや、ブラウザーで表示可能な他のリソースを表示するために使用できます。
Firefox では、 PDF が iframe 内に埋め込まれて表示されるようにするには (ダウンロードファイルとして提案されるのではなく)、pdfjs.disabled
の設定をfalse
に設定する必要があります。
<iframe></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);