以前の記事で示したとおり、HTML フォームは HTTP リクエストを宣言的な方法で設定するのに便利です。しかし多くの場合、フォームは JavaScript を使用した HTTP リクエストの設定でも有用になり得ます。これを扱う方法はいくつかありますので、本記事で説明します。
フォームは必ずしもフォームであるとは限らない
open Web apps が現れたことで HTML フォームを、人間が記入する文字どおりのフォーム以外に使用することが次第に一般的になってきました。ますます多くの開発者が、伝送するデータの処理を制御しようとしています。
グローバルインターフェイスの制御
データの送信方法を制御する主な理由は、ユーザインターフェイス (UI) にあります。HTML フォームの標準的な使用方法ではデータを送信したページの読み込みが必要であり、これはページ全体の更新が必要であるということです。多くの現代的なインターフェイスでそれは、ユーザにスムーズな体験を提供するため、ちらつきやネットワークの遅延を隠すことにより避けたいのが一般的です。
そのために、多くの現代的な UI は HTML フォームをユーザからデータを集めるためだけに使用します。ユーザ側でデータを送信する準備ができると、アプリケーションはバックグラウンドで非同期にデータを制御および送信して、UI は変更が必要な部分にのみ対処します。
任意のデータを非同期に送信することは、"Asynchronous JavaScript And XML" を表す頭字語である AJAX として知られています。
その違いは?
AJAX 技術は主に、XMLHttpRequest
(XHR) DOM オブジェクトに依存します。このオブジェクトは HTTP リクエストの構築、送信、そしてレスポンスの取得を可能にする強力なツールです。
注記: XMLHttpRequest
DOM オブジェクトに依存しない AJAX 技術もあります。例えば JSONP (日本語版) と eval()
関数の組み合わせを利用できます。これも動作しますが、重大なセキュリティの問題を引き起こす可能性があるため推奨しません。唯一の理由は古いブラウザが XMLHttpRequest
や JSON をサポートしていないことへの対処ですが、それはとても古いブラウザです! このような技術は避けるべきです。
歴史的に見ると、XMLHttpRequest
は交換形式として XML の使用を想定していました。しかし圧倒的な採用率により、JSON が XML に取って代わりました。とはいえ、この選択に正否はありません: JSON は軽量で構造化された形式、XML はよりセマンティックな形式です。JSON はネットワークのフットプリントを小さくしたい場合によい選択肢です。一方 XML は、データ自身やデータの構造についてより多くの情報を提供します。その選択はあなた次第です。
しかしどちらの場合でも、このような構造化データはフォームデータのリクエストの構造には適合しません。もっとも基本的なフォームでは、フォームデータのリクエストが URL エンコードされたキーと値のペアのリストになります。バイナリデータについては、HTTP リクエスト全体がそれを扱うために作り変えられます。
フロントエンド (すなわち、ブラウザで実行されるコード) とバックエンド (Web サーバで実行されるコード) の両方を制御する場合は、適合するデータ構造を両側で定義できることから問題にはなりません。
しかし残念ながら、サードパーティのサービスを利用したい場合は、それは容易ではありません。時にはフォームデータのリクエストとしてのみデータを受け入れるサービスを使用しなければなりません。フォームデータだけを扱うほうがより簡単である場合もあります。データがキーと値のペアで構成される場合やバイナリデータを送信したい場合は、そのような性質のデータを扱うためのバックエンドツールがすでに多くあります。
ではどのようにしてそのようなデータを送信できるのでしょうか?
フォームデータの送信
フォームデータを送信するための実際の方法が、古い技術からより新しい FormData
オブジェクトまで、3 種類あります。これらの技術を詳しく見ていきましょう。
隠し iframe での DOM 作成
プログラムによってフォームデータを送信するためのもっとも古典的な方法は、DOM API を使用してフォームを構築して、そのデータを隠し <iframe>
に送信することです。送信したデータの結果にアクセスしたい場合に必要なことは、隠し <iframe>
の内容物を取得することだけです。
警告: この手法には多くの欠点があり、使用を避けるべきです。この方法はスクリプトインジェクション攻撃 (日本語版) を可能にするため、サードパーティのサービスを使用する場合は潜在的なリスクになります。HTTPS を使用する場合は同一生成元ポリシーの影響を受けて、<iframe>
の内容物にアクセスできなくなる場合があります。しかし、この方法はとても古いブラウザでも動作するでしょうし、旧式のソフトウェアのサポートが必要である場合は唯一の選択肢になるでしょう。
以下は簡単な例です:
<button onclick="sendData({test:'ok'})">Click Me!</button>
すべてのマジックはスクリプト内にあります:
// データの送信に使用する iFrame を作成しましょう var iframe = document.createElement("iframe"); iframe.name = "myTarget"; // 次に、主ドキュメントに iframe を付加します window.addEventListener("load", function () { iframe.style.display = "none"; document.body.appendChild(iframe); }); // これは実際にデータを送信するために使用する関数です // 引数は 1 つあり、それはキーと値のペアを持っているオブジェクトです。 function sendData(data) { var name, form = document.createElement("form"), node = document.createElement("input"); // レスポンスが読み込まれたときにする行うべきことを定義します iframe.addEventListener("load", function () { alert("Yeah! Data sent."); }); form.action = "https://www.cs.tut.fi/cgi-bin/run/~jkorpela/echo.cgi"; form.target = iframe.name; for(name in data) { node.name = name; node.value = data[name].toString(); form.appendChild(node.cloneNode()); } // フォームは送信するために、主ドキュメントに付加することが必要です form.style.display = "none"; document.body.appendChild(form); form.submit(); // しかしフォームを送信した後は、置いたままにしても役に立ちません document.body.removeChild(form); }
そして、結果は以下のとおりです:
手作業での XMLHttpRequest の作成
XMLHttpRequest
は HTTP リクエストを扱うための、もっとも安全かつ頼りになる方法です。フォームデータを XMLHttpRequest
で送信するには、データの適切にエンコードすることが必要です。すべてのデータは URL エンコードしなければなりません。また、フォームデータのリクエストに関する詳細を知っていることも必要です。
注記: XMLHttpRequest
の使用について詳しく知りたい場合に、興味を持つであろう記事が 2 つあります: AJAX の入門記事とXMLHttpRequest 自体の使用に関するより高度な記事です。
先ほどの例を再構築しましょう:
<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>
このとおり、HTML はまったく変わりません。ところが、背後の JavaScript コードは完全に変わっています:
function sendData(data) { var XHR = new XMLHttpRequest(); var urlEncodedData = ""; var urlEncodedDataPairs = []; var name; // data オブジェクトを、URL エンコードしたキーと値のペアの配列に変換します for(name in data) { urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name])); } // キーと値のペアをひとつの文字列に連結して、Web ブラウザのフォーム送信方式に // 合うよう、エンコードされた空白をプラス記号に置き換えます。 urlEncodedData = urlEncodedDataPairs.join('&').replace(/%20/g, '+'); // データが正常に送信された場合に行うことを定義します XHR.addEventListener('load', function(event) { alert('Yeah! Data sent and response loaded.'); }); // エラーが発生した場合に行うことを定義します XHR.addEventListener('error', function(event) { alert('Oups! Something goes wrong.'); }); // リクエストをセットアップします XHR.open('POST', 'https://ucommbieber.unl.edu/CORS/cors.php'); // フォームデータの POST リクエストを扱うために必要な HTTP ヘッダを追加します XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); XHR.setRequestHeader('Content-Length', urlEncodedData.length); // 最後に、データを送信します XHR.send(urlEncodedData); }
そして、結果は以下のとおりです:
注記: この種類の XMLHttpRequest
の使い方は、サードパーティの Web サイトへデータを送信したい場合に同一生成元ポリシーの影響を受けやすくなります。生成元を越えるリクエストを行うには、CORS や HTTP アクセスコントロールを習熟するべきです。
XMLHttpRequest と FormData オブジェクトの使用
HTTP リクエストを手作業で構築するのはやや面倒です。幸い、最近の XMLHttpRequest 仕様の進歩によりフォームデータのリクエストを扱うための便利かつよりシンプルな方法がもたらされました。それが FormData
オブジェクトです。
FormData
オブジェクトは 2 つの方法で使用できます。伝送するデータのセットを構築するために使用したり、指定したフォーム要素によって表されるデータを取得したりするために使用でき、これによりデータをどのように送信するかを制御できます。FormData
オブジェクトは "書き込み専用" のオブジェクトであり、変更はできても内容物を取り出すことはできない点は注目に値します。
この種類のオブジェクトの詳しい使い方は Using FormData Objects で説明しますが、ここで簡単な例を 2 つ示します:
独立した FormData オブジェクトを使用する
<button type="button" onclick="sendData({test:'ok'})">Click Me!</button>
HTML のサンプルはおわかりでしょう。
function sendData(data) { var XHR = new XMLHttpRequest(); var FD = new FormData(); // データを FormData オブジェクトに投入します for(name in data) { FD.append(name, data[name]); } // データが正常に送信された場合に行うことを定義します XHR.addEventListener('load', function(event) { alert('Yeah! Data sent and response loaded.'); }); // エラーが発生した場合に行うことを定義します XHR.addEventListener('error', function(event) { alert('Oups! Something goes wrong.'); }); // リクエストをセットアップします XHR.open('POST', 'https://ucommbieber.unl.edu/CORS/cors.php'); // FormData オブジェクトを送信するだけです。HTTP ヘッダは自動的に設定されます XHR.send(FD); }
そして、結果は以下のとおりです:
form 要素に紐づけた FormData を使用する
FormData
オブジェクトを <form>
要素に紐づけることもできます。これにより、フォームに含まれるデータを表す FormData
をすばやく得ることができます。
HTML の部分はかなり典型的です:
<form id="myForm"> <label for="myName">Send me your name:</label> <input id="myName" name="name" value="John"> <input type="submit" value="Send Me!"> </form>
しかし、JavaScript がフォームを乗っ取ります。
window.addEventListener("load", function () { function sendData() { var XHR = new XMLHttpRequest(); // FormData オブジェクトと form 要素を紐づけます var FD = new FormData(form); // データが正常に送信された場合に行うことを定義します XHR.addEventListener("load", function(event) { alert(event.target.responseText); }); // エラーが発生した場合に行うことを定義します XHR.addEventListener("error", function(event) { alert('Oups! Something goes wrong.'); }); // リクエストをセットアップします XHR.open("POST", "https://ucommbieber.unl.edu/CORS/cors.php"); // 送信したデータは、ユーザがフォームで提供したものです XHR.send(FD); } // form 要素にアクセスしなければなりません var form = document.getElementById("myForm"); // フォームの submit イベントを乗っ取ります form.addEventListener("submit", function (event) { event.preventDefault(); sendData(); }); });
そして、結果は以下のとおりです:
バイナリデータを扱う
最後はバイナリデータについてです。ファイルウィジェットを含むフォームに紐づけた FormData
オブジェクトを使用する場合、データは自動的に処理されますが、手動でバイナリデータを送信したい場合に行う追加作業がいくつかあります。
現代の Web デザインではバイナリデータの提供元として考えられるものがいくつもあります: FileReader
API、Canvas
API、WebRTC API が一般的です。しかし残念なことに、一部の古いブラウザはバイナリデータにアクセスできなかったり、複雑な回避策が必要になったりします。古いブラウザでのバイナリデータへのアクセスは、本記事で扱う範囲を超えます。FileReader
API について詳しく知りたい場合は、Web アプリケーションからファイルを扱うをご覧いただくとよいでしょう。
一方、FormData
をサポートするブラウザでのバイナリデータ送信はまったく難しくありません。通常は append()
メソッドを使用するだけで済みます。ただし、手動でそれを行わなければならない場合は難しくなります。
以下の例ではバイナリデータへのアクセスに FileReader
API を使用しており、また手作業でマルチパートのフォームデータを作成しています:
<form id="myForm"> <p> <label for="i1">text data:</label> <input id="i1" name="myText" value="Some text data"> </p> <p> <label for="i2">file data:</label> <input id="i2" name="myFile" type="file"> </p> <button>Send Me!</button> </form>
ご覧のとおり、HTML はごく標準的なフォームです。不思議なところはありません。"マジック" は JavaScript コードの中にあります:
// DOM ノードにアクセスしたいため、 // ページをロードしたときにスクリプトを初期化します。 window.addEventListener('load', function () { // この変数は、フォームデータを格納するために使用します。 var text = document.getElementById("i1");; var file = { dom : document.getElementById("i2"), binary : null }; // ファイルコンテンツへのアクセスに FileReader API を使用します。 var reader = new FileReader(); // FileReader API は非同期であるため、ファイルの読み取りが完了したときに // その結果を保存しなければなりません。 reader.addEventListener("load", function () { file.binary = reader.result; }); // ページを読み込んだとき、すでに選択されているファイルがあればそれを読み取ります。 if(file.dom.files[0]) { reader.readAsBinaryString(file.dom.files[0]); } // 一方、ユーザがファイルを選択したらそれを読み取ります。 file.dom.addEventListener("change", function () { if(reader.readyState === FileReader.LOADING) { reader.abort(); } reader.readAsBinaryString(file.dom.files[0]); }); // sendData 関数がメインの関数です。 function sendData() { // 始めに、ファイルが選択されている場合はファイルの読み取りを待たなければなりません。 // そうでない場合は、関数の実行を遅延させます。 if(!file.binary && file.dom.files.length > 0) { setTimeout(sendData, 10); return; } // マルチパートのフォームデータリクエストを構築するため、 // XMLHttpRequest のインスタンスが必要です。 var XHR = new XMLHttpRequest(); // リクエストの各パートを定義するためのセパレータが必要です。 var boundary = "blob"; // 文字列としてリクエストのボディを格納します。 var data = ""; // そして、ユーザがファイルを選択したときに if (file.dom.files[0]) { // リクエストのボディに新たなパートを作ります data += "--" + boundary + "\r\n"; // フォームデータであることを示します (他のものになる場合もあります) data += 'content-disposition: form-data; ' // フォームデータの名前を定義します + 'name="' + file.dom.name + '"; ' // 実際のファイル名を与えます + 'filename="' + file.dom.files[0].name + '"\r\n'; // ファイルの MIME タイプを与えます data += 'Content-Type: ' + file.dom.files[0].type + '\r\n'; // メタデータとデータの間に空行を置きます data += '\r\n'; // リクエストのボディにバイナリデータを置きます data += file.binary + '\r\n'; } // テキストデータの場合はシンプルです。 // リクエストのボディに新たなパートを作ります data += "--" + boundary + "\r\n"; // フォームデータであることと、データの名前を示します。 data += 'content-disposition: form-data; name="' + text.name + '"\r\n'; // メタデータとデータの間に空行を置きます data += '\r\n'; // リクエストのボディにテキストデータを置きます。 data += text.value + "\r\n"; // 完了したら、リクエストのボディを "閉じます"。 data += "--" + boundary + "--"; // データが正常に送信された場合に行うことを定義します XHR.addEventListener('load', function(event) { alert('Yeah! Data sent and response loaded.'); }); // エラーが発生した場合に行うことを定義します XHR.addEventListener('error', function(event) { alert('Oups! Something goes wrong.'); }); // リクエストをセットアップします XHR.open('POST', 'https://ucommbieber.unl.edu/CORS/cors.php'); // マルチパートのフォームデータの POST リクエストを扱うために必要な HTTP ヘッダを追加します。 XHR.setRequestHeader('Content-Type','multipart/form-data; boundary=' + boundary); XHR.setRequestHeader('Content-Length', data.length); // 最後に、データを送信します // Firefox のバグ 416178 により、send() の代わりに sendAsBinary() を使用することが必要です。 XHR.sendAsBinary(data); } // 少なくとも、フォームにアクセスしなければなりません。 var form = document.getElementById("myForm"); // submit イベントを乗っ取ります。 form.addEventListener('submit', function (event) { event.preventDefault(); sendData(); }); });
そして、結果は以下のとおりです:
sendAsBinary
は Gecko 31 (Firefox 31 / Thunderbird 31 / SeaMonkey 2.28) で非推奨とされており、まもなく削除する予定です。代わりに、標準化されたメソッドである send(Blob data)
を使用できます。おわりに
対象にしたいブラウザによって、JavaScript によるフォームデータの送信は容易であったり実に困難であったりします。新しいブラウザのみを考慮するなら、とてもシンプルになるでしょう。しかし古いブラウザをサポートすることも必要なら、より複雑になります。一般的に FormData
はあなたが抱えているすべての問題の答えになり、また古いブラウザ向けのポリフィルを使用することをためらうべきではありません:
- こちらの GIST は、
Web Workers
によるFormData
のポリフィルです。 - HTML5-formdata は
FormData
オブジェクトのポリフィルの試みですが、File API のサポートが必要です。