JavaScript によるフォームの送信

前回の記事と同様に、HTML フォームは HTTP リクエストを宣言的に送信できます。しかし、フォームは JavaScript 経由で送信する HTTP リクエストを準備することもできます。この記事ではその方法を探ります。

フォームは必ずしもフォームであるとは限らない

open Web apps が現れたことで HTML フォームを、人が記入する文字どおりのフォーム以外に使用することが次第に一般的になってきました。ますます多くの開発者が、伝送するデータの処理を制御しようとしています。

グローバルインターフェイスの制御を取得

標準の HTML フォーム送信では、データが送信された URL がロードされます。つまり、ブラウザウィンドウは全ページロードで移動します。ページ全体の読み込みを回避すると、ちらつきやネットワークの遅延を隠して、よりスムーズな操作を提供できます。

最近の多くの UI は、HTML フォームを使用してユーザからの入力を収集します。ユーザがデータを送信しようとすると、アプリケーションはバックグラウンドでデータを非同期的に制御して送信し、変更が必要な UI の部分のみを更新します。

任意のデータを非同期に送信することは、"Asynchronous JavaScript And XML" を表す頭字語である AJAX として知られています。

その違いは?

AJAXXMLHttpRequest (XHR) DOM オブジェクトを使用します。HTTP リクエストを作成して送信し、結果を取得することができます。

注記: 古い AJAX 技術は XMLHttpRequest に依存しないかもしれません。たとえば、JSONPeval() 関数を組み合わせたものです。うまくいきますが、深刻なセキュリティ問題のためにお勧めできません。使用する唯一の理由は、XMLHttpRequest や JSON をサポートしていない従来のブラウザ用ですが、それらは非常に古いブラウザです。そのようなテクニックは避けてください

歴史的には、XMLHttpRequest は交換フォーマットとして XML を取得して送信するように設計されていました。しかし、JSON は XML に取って代わり、今日では圧倒的に一般的になっています。

しかし、XML も JSON もフォームデータリクエストのエンコーディングには適合しません。フォームデータ (application/x-www-form-urlencoded) は、キーと値のペアの URL エンコードリストで構成されています。バイナリデータを送信するために、HTTP リクエストは multipart/form-data に再形成されます。

フロントエンド (ブラウザで実行されるコード) とバックエンド (サーバで実行されるコード) を制御すれば、JSON/XML を送信して必要に応じて処理することができます。

しかし、サードパーティのサービスを利用したい場合、それほど簡単ではありません。一部のサービスはフォームデータのみを受け付けます。フォームデータを使用する方が簡単な場合もあります。データがキー/値のペア、または生のバイナリデータである場合、既存のバックエンドツールは追加のコードを必要とせずにそれを処理できます。

ではどのようにしてそのようなデータを送信するのでしょうか?

フォームデータの送信

フォームデータを送信するには、従来の方法から新しい FormData オブジェクトまで3つの方法があります。それらを詳しく見てみましょう。

手作業での XMLHttpRequest の作成

XMLHttpRequest は、HTTP リクエストを作成する最も安全で信頼性の高い方法です。XMLHttpRequest を使用してフォームデータを送信するには、URL エンコードしたデータを準備し、フォームデータリクエストの詳細に従ってください。

注記: XMLHttpRequest についてさらに学ぶ場合、これらの記事に興味を持つかもしれません: 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', 'http://ucommbieber.unl.edu/CORS/cors.php');

  // フォームデータの POST リクエストを扱うために必要な HTTP ヘッダを追加します
  XHR.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

  // 最後に、データを送信します
  XHR.send(urlEncodedData);
}

そして、結果は以下のとおりです:

注記: この XMLHttpRequest の使用は、第三者の Web サイトにデータを送信したい場合にも、同一生成元ポリシーの対象となります。クロスオリジンリクエストの場合は、CORS と HTTP のアクセス制御が必要です。

XMLHttpRequest と FormData オブジェクトの使用

HTTP リクエストを手作業で作成するのは大変なことです。 幸いなことに、最近の XMLHttpRequest 仕様では FormData オブジェクトを使ってフォームデータリクエストを処理する便利で簡単な方法が提供されています。

FormData オブジェクトは、送信用のフォームデータを作成したり、送信方法を管理するフォーム要素内のデータを取得するために使用できます。FormData オブジェクトは "書き込み専用" であることに注意してください。つまり、変更することはできますが、内容を取得することはできません。

このオブジェクトの使い方は FormData オブジェクトの使用で詳述されていますが、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', 'http://ucommbieber.unl.edu/CORS/cors.php');

  // FormData オブジェクトを送信するだけです。HTTP ヘッダは自動的に設定されます
  XHR.send(FD);
}

そして、結果は以下のとおりです:

form 要素に紐づけた FormData を使用する

FormData オブジェクトを <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", "http://ucommbieber.unl.edu/CORS/cors.php");

    // 送信したデータは、ユーザがフォームで提供したものです
    XHR.send(FD);
  }
 
  // form 要素にアクセスしなければなりません
  var form = document.getElementById("myForm");

  // フォームの submit イベントを乗っ取ります
  form.addEventListener("submit", function (event) {
    event.preventDefault();

    sendData();
  });
});

そして、結果は以下のとおりです:

フォームの elements プロパティを使用してフォーム内のすべてのデータ要素のリストを取得し、それらを一度に1つずつ手動で管理することで、このプロセスにさらに関わることができます。詳細については、要素リストの内容にアクセスする in HTMLFormElement.elementsの例を参照してください。

 

隠し 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 = "http://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);
}

そして、結果は以下のとおりです:

バイナリデータを扱う

<input type="file"> ウィジェットを含むフォームで FormData オブジェクトを使用すると、データは自動的に処理されます。しかし、バイナリデータを手動で送るには、追加でやるべきことがあります。

現代の Web には、バイナリデータのソースが多数あります。たとえば、FileReaderCanvasWebRTC などです。残念ながら、一部の従来のブラウザではバイナリデータにアクセスできないか、または複雑な回避策が必要です。これらのレガシーケースはこの記事の範囲外です。FileReader API について詳しく知りたい場合は、Web アプリケーションからファイルを扱うを読んでください。

FormData をサポートするバイナリデータを送信するのは簡単です。append() メソッドを使用すれば完了です。手動でやらなければならないならば、それはトリッキーです。

以下の例ではバイナリデータへのアクセスに FileReader API を使用しており、また手作業でマルチパートのフォームデータを作成しています:

次の例では、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は標準の <form> です。不思議なところは何もありません。「魔法」は 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', 'http://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();
  });
});

そして、結果は以下のとおりです:

おわりに

ブラウザによっては、JavaScript を介してフォームデータを送信するのが簡単な場合と難しい場合があります。FormData オブジェクトが一般的な答えであり、レガシーブラウザでポリフィルを使用することをためらってはいけません。

  • この gist は、FormDataWeb Workers を入力します。
  • HTML5-formdataFormData オブジェクトに入力しますが、File API が必要です。
  • この polyfill は FormData が持っているほとんどの新しいメソッド (エントリ、キー、値、for...of のサポート) を提供します。

このモジュール

ドキュメントのタグと貢献者

このページの貢献者: silverskyvicto, mdnwebdocs-bot, dlwe, chrisdavidmills, yyss, ethertank
最終更新者: silverskyvicto,