Join MDN and developers like you at Mozilla's View Source conference, 12-14 September in Berlin, Germany. Learn more at https://viewsourceconf.org

JavaScript によるフォームの送信

以前の記事で示したとおり、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() 関数の組み合わせを利用できます。これも動作しますが、重大なセキュリティの問題を引き起こす可能性があるため推奨しません。唯一の理由は古いブラウザが XMLHttpRequestJSON をサポートしていないことへの対処ですが、それはとても古いブラウザです! このような技術は避けるべきです

歴史的に見ると、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 = "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);
}

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

手作業での 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', 'http://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', 'http://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", "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();
  });
});

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

バイナリデータを扱う

最後はバイナリデータについてです。ファイルウィジェットを含むフォームに紐づけた 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', '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();
  });
});

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

注記: 上記の例にある非標準メソッド sendAsBinary は Gecko 31 (Firefox 31 / Thunderbird 31 / SeaMonkey 2.28) で非推奨とされており、まもなく削除する予定です。代わりに、標準化されたメソッドである send(Blob data) を使用できます。

おわりに

対象にしたいブラウザによって、JavaScript によるフォームデータの送信は容易であったり実に困難であったりします。新しいブラウザのみを考慮するなら、とてもシンプルになるでしょう。しかし古いブラウザをサポートすることも必要なら、より複雑になります。一般的に FormData はあなたが抱えているすべての問題の答えになり、また古いブラウザ向けのポリフィルを使用することをためらうべきではありません:

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

 このページの貢献者: yyss, ethertank
 最終更新者: yyss,